分布式Java应用基础与实践-笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/t1g2q3/article/details/84893454

分布式Java应用基础与实践

  • 类加载机制:JVM将类加载过程分为三个步骤:装载、链接和初始化。
  • https://img.mubu.com/document_image/67864e07-5f28-49d7-aa29-4e29dc8b459c-67246.jpg
    • 1、装载Load
      • 装载过程负责找到二进制字节码并加载至JVM中,JVM通过类的全限定名及类加载器完成类的加载,同样,也采用以上两个元素来标识一个被加载了的类:类的全限定名+ClassLoader实例ID。
    • 2、链接Link
      • 链接过程负责对二进制字节码的格式进行校验、初始化装载类中的静态变量及解析类中调用的接口、类。二进制字节码的格式校验遵循JVM规范,如果格式不符合,则抛出VerifyError;校验过程中如果碰到要引用到其他的接口和类,也会进行加载;如果加载过程失败,则会抛出NoClassDefFoundError。在完成校验后,JVM初始化类中的静态变量,并将其值赋为默认值。最后对类中的所有属性、方法进行验证,以确保其要调用的属性、方法存在,以及具备相应的权限(例如public、private域权限等)。如果这个阶段失败,可能会造成NoSuchMethodEror、NoSuchFieldError。
    • 3、初始化Initialize
      • 初始化过程即执行类中的静态初始化代码、构造器代码及静态属性的初始化,在以下四种情况下初始化过程会被出发执行:
        • a. 调用了new;
        • b. 反射调用了类中的方法;
        • c. 子类调用了初始化;
        • d. JVM启动过程中指定的初始化类。
      • 在执行初始化过程之前,首先必须完成链接过程中的校验和准备阶段,解析阶段则不强制。
  • 为什么我们要自定义类加载器?
    • 举一个例子来说吧,主 流的Java Web服务器,比如Tomcat,都实现了自定义的类加载器。因为一个功能健全的Web服务器,要解决如下几个问题:
    • 1、部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。这是最基本的要求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相使用。
    • 2、部署在同一个服务器上的两个Web应用程序所使用的Java类库可以相互共享。这个需求也很常见,比如相同的Spring类库10个应用程序在用不可能分别存放在各个应用程序的隔离目录中。
    • 3、支持热替换,我们知道JSP文件最终要编译成.class文件才能由虚拟机执行,但JSP文件由于其纯文本存储特性,运行时修改的概率远远大于第三方类库或自身.class文件,而且JSP这种网页应用也把修改后无须重启作为一个很大的优势看待。
  • Java开发人员调用Class.forName来获取一个对应名称的Class对象时,JVM会从方法栈上寻找第一个ClassLoader,通常也就是执行Class.forName所在类的ClassLoader,并使用此ClassLoader来加载此名称的类。JVM为了保护加载、执行的类的安全,它不允许ClassLoader直接卸载加载了的类,只有JVM才能卸载,在Sun JDK中,只有当ClassLoader对象没有引用时,此ClassLoader对象加载的类才会被卸载。
  • 类加载方面的常见异常
    • 1、ClassNotFoundException
      • 产生这个异常的原因是在当前的ClassLoader中加载类时未找到类文件,对位于System ClassLoader的类很容易判断,只要加载的类不再Classpath中,而对位于User-Defined ClassLoader的类则麻烦些,要具体查看这个ClassLoader加载类的过程,才能判断此ClassLoader要从什么位置加载到此类。
    • 2、NoClassDefFoundError
      • 造成此异常的主要原因是加载的类中引用到的另外的类不存在,例如要加载A,而A中调用了B,B不存在或当前ClassLoader没法加载B,就会抛出这个异常。因此,对于这个异常,须先查看是加载那个类时报出的,然后再确认该类中引用的类是否存在于当前ClassLoader能加载到的位置。
    • 3、LinkageError
      • 该异常在自定义ClassLoader的情况下更容易出现,主要原因是此类已经在ClassLoader加载过了,重复地加载会造成该异常,因此要注意避免在并发的情况下出现这样的问题。
      • 由于JVM的这个保护机制,使得在JVM中没办法直接更新一个已经load的Class,只能创建一个新的ClassLoader来加载更新的Class,然后将新的请求转入该ClassLoader来获取类,这也是JVM中不要实现动态更新的原因之一,而其他更多的原因时对象状态的复制、依赖的设置等。
    • 4、ClassCaseException
      • 该异常有多种原因,在JDK 5支持泛型后,合理使用泛型可相对减少此异常的触发。这些原因中比较难查的时两个A对象由不同的ClassLoader加载的情况,这时如果将其中某个A对象造型成另外一个A对象,也会报出ClassCaseException。
  • 类执行机制
    • 在完成将class文件信息加载到JVM并产生Class对象后,就可执行Class对象的静态方法或实例化对象进行调用了。在源码编译阶段将源码编译为JVM字节码,JVM字节码时一种中间代码的方式,要由JVM在运行期对其进行解释并执行,这种方式称为字节码解释执行方式。
    • Sun JDK基于栈的体系结构来执行字节码,基于栈方式的好处为代码紧凑,体积小。
    • 线程在创建后,都会产生程序计数器(PC)和栈(Stack);PC存放了下一条要执行的指令在方法内的偏移量;栈中存放了栈帧(StackFrame),每个方法每次调用都会产生栈帧。栈帧主要分为局部变量区和操作数栈两部分,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果,栈帧中还会有一些杂用空间,例如只想方法已解析的常量池的引用、其他一些VM内部实现需要的数据等。
  • 对象引用关系
    • 强引用
    • A a = new A();就是一个强引用,强引用的对象只有在主动释放引用后才会被GC。
    • 软引用
    • 软引用采用SoftReference来实现,采用软引用来建立引用的对象,在JVM内存不足时会被回收,因此很适合用于实现缓存。另外,当GC认为扫描到的SoftReference不经常使用时,也会进行回收。
    • 弱引用
    • 弱引用采用WeakReference来实现,采用弱引用建立引用的对象没有强引用后,GC时即会被自动释放。ThreadLocal的实现。
    • 虚引用
    • 虚引用采用PhantomReference来实现,采用虚引用可追踪到对象是否已从内存中被删除。
  • CPU消耗分析
    • a. 上下文切换
      • 每个CPU(或多核CPU中的每核CPU)在同一时间只能执行一个线程,Linux采用的是抢占式调用。即为每个线程分配一定的执行时间,当到达执行时间、线程中有IO阻塞或高优先级进程要执行时,Linux将切换执行的线程,在切换时要存储目前线程的执行状态,并恢复要执行的线程的状态,这个过程就称为上下文切换。对于Java应用,典型的是在进行文件IO操作、网络IO操作、锁等待或线程Sleep时,当前线程会进入阻塞或休眠状态,从而触发上下文切换,上下文切换过多会造成内核占据较多的CPU使用,使得应用的响应速度下降。
    • b. 运行队列
      • 每个CPU核都维护了一个可运行的线程队列,例如一个4核的CPU,Java应用中启动了8个线程,且这8个线程都处于可运行状态,那么在分配平均的情况下每个CPU中的运行队列里就会有两个线程。通常而言,系统的load主要由CPU的运行队列来决定,假设以上状况维持了一分钟,那么这一分钟内系统的load就是2,但由于load是个复杂的值,因此也不是绝对的,运行队列值越大,就意味着线程会要消耗越长时间才能执行完。
    • c. 利用率
      • 在Linux中,可通过top命令查看CPU的消耗情况。
      • 对于Java应用而言,CPU消耗严重主要体现在us、sy两个值上。
      • us
        • 当us值过高时,表示运行的应用消耗了大部分CPU,在这种情况下,对于Java应用而言,最重要的是找到具体消耗CPU的线程所执行的代码。
      • sy
        • 当sy值高时,表示Linux花费了更多的时间在进行线程切换,Java应用造成这种现象的主要原因时启动的线程比较多,且这些线程多数都处于不断的阻塞(例如锁等待、IO等待状态)和执行状态的变化过程中,这就导致了操作系统要不断地切换执行的线程,产生大量的上下文切换。
  • CPU消耗高的解决方法
    • 1CPU us高的解决方法
      • CPU us高的原因主要是执行线程无任何挂起动作,且一致执行,导致CPU没有机会去调度执行其他的线程,造成线程饿死的现象。对于这种情况,常见的一种优化方法时对这种线程的动作增加Thread.sleep,以释放CPU的执行权,降低CPU的消耗。
      • 在实际的Java应用中会有很多类似的场景,例如多线程的任务执行管理器,它通常要通过扫描任何集合列表来执行任务。对于类似的场景,都可通过增加一定的sleep时间来避免消耗过多的CPU。
      • 除了上面的场景外,还有一种经典的场景时状态的扫描,例如某线程要等其他线程改变了值后才可继续执行。对于这种场景,最佳的方式是改为wait/notify机制。
      • 对于其他类似循环次数太多、正则、计算等造成的CPU us过高的情况,则要结合业务需求来进行调优。
      • 对于GC频繁造成的CPU us高的现象,则要通过JVM调优或程序调优,降低GC的执行次数。
    • 2CPU sy高的解决方法
      • CPU sy高的原因主要是线程的运行状态要经常切换,对于这种情况,最简单的优化方法是减少线程数。减少线程数是能让sy值下降的,所以不是线程数越多吞吐量就越高,线程数需要设置为合理的值。
      • 造成CPU sy高的原因除了启动的线程过多以外,还有一个重要的原因是线程之间的锁竞争激烈,造成了线程状态经常要切换,因此尽可能降低线程之间的锁竞争也是常见的优化方法。锁竞争降低后,线程的状态切换次数也就会下降,sy值会相应下降。但值得注意的是,如果线程数过多,调优后可能造成us值过高,所以合理地设置线程数非常关键。锁竞争更有可能造成系统资源消耗不多,但系统性能不足的现象。
  • 协程
    • 对于分布式Java应用而言,还有一种典型的现象是应用中由较多的网络IO操作或确实需要一些锁竞争机制(例如数据库连接池),但为了能够支持高的并发量,在Java应用中只能借助启动更多的线程来支撑。在这样的情况下当并发量增长到一定程度后,可能会造成CPU sy高的现象,对于这种现象,可采用协程来支撑更高的并发量,避免并发量上涨后造成CPU sy消耗严重、系统load迅速上涨和系统性能下降。
    • 在目前的Sun JDK中,创建并启动一个Thread对象就意味着运行了一个原生线程,当这个线程中由任何阻塞动作(例如同步文件IO、同步网络IO、锁等待、Thread.sleep等)时,这个线程就会被挂起,但仍然占据着线程的资源。当线程中的阻塞动作完成时,由操作系统来恢复线程的上下文,并调用执行,这是一种标准的遵循目前操作系统的实现方式,这种方式对于Java应用而言,当并发量上涨后,有可能出现的现象是启动的大量线程都处于浪费状态。例如一个线程在等待数据库执行结果的返回,如这个数据库执行操作需要花费2秒,那么就意味着这个线程资源被白白占用了2秒,一方面导致了其他的请求只能是放在缓冲队列中等待执行,性能下降;另一方面是造成系统中线程切换频繁,CPU运行队列过长,协程要改变的就是不浪费相对昂贵的原生线程资源。
  • 文件IO消耗严重的解决方法
    • 1、异步写文件
      • 将写文件的同步动作改为异步动作,避免应用由于写文件慢而性能下降太多,例如写日志,可以使用log4j提供的AsyncAppender。
    • 2、批量读写
      • 频繁的读写操作对IO消耗会很严重,批量操作将大幅度提升IO操作的性能。
    • 3、限流
      • 频繁读写的另外一个调优方式是限流,从而将文件IO消耗控制在一个能接受的范围。
  • 网络IO消耗严重的解决方法
    • 造成网络IO消耗严重的主要原因是同时需要发送或接收的包太多。对于这类情况,常用的调优方法为进行限流,限流通常是限制发送packet的频率,从而在网络IO消耗可接受的情况下来发送packet。
  • 对于内存消耗严重的情况
    • 1、释放不必要的内存
      • 内存消耗严重的情况中最典型的一种现象是代码中持有了不需要的对象引用,造成这些对象无法被GC,从而占据了JVM堆内存。这种情况最典型的一个例子是在复用线程的情况下使用ThreadLocal,由于线程复用,ThreadLocal中存放的对象如未做主动释放的话则不会被GC。
    • 2、使用对象缓存池
    • 3、采用合理的缓存失效算法(FIFO、LRU、LFU等)
    • 4、合理使用SoftReference和WeakReference
  • 对于资源消耗不多,但是程序执行慢的情况
    • 1、锁竞争激烈
      • a. 使用并法包中的类
      • b. 使用Treiber算法非阻塞栈算法
      • c. 使用Michael-Scott非阻塞队列算法
      • d. 尽可能少用锁
      • e. 拆分锁
      • f. 去除读写操作的互斥锁
    • 2、未充分使用硬件资源
      • a. 未充分使用CPU(在能并行处理的场景中未使用足够的线程)
      • b. 未充分使用内存
        • 未充分使用内存的场景非常多,如数据的缓存、耗时资源的缓存(例如数据库连接的创建、网络连接的创建)、页面片段的缓存等。但也要避免内存资源的过度使用,在内存资源可接受、GC频率及系统结构(例如集群环境可能会带来缓存的同步等)可接受的情况下,应充分使用内存来缓存数据,提升系统的性能。

猜你喜欢

转载自blog.csdn.net/t1g2q3/article/details/84893454