欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

性能监控/优化系列——HotSpot JVM相关

程序员文章站 2022-07-14 12:19:39
...

 1. HotSpot JVM在进化的过程中所做的优化:JIT compilers,sophisticated garbage collectors,JVM runtime environment。

2. HotSpot VM:三个主要组件:VM Runtime, JIT compiler, Memory manager(Garbage Collector)。后两种都为前者的插件,runtime提供了一些调用插件逻辑的接口,既然是插件,那么必然会有很多选择,因此在配制JVM的时候可以通过参数的形式指定它们,如垃圾回收就有很多实现方式。
3. JIT(字节码—>机器指令) 针对较常被执行的程式码进行编译,其余部分仍使用转译器来执行,被编译的部分优化成相当精简的原生型指令码(native code),大大提高了执行效率。
4. 本来HotSpot VM的64位版本性能比32位的版本要差,原因是JVM内部一个叫oops对象的size变得更大了,造成CPU缓存的利用率下降。后来采用了对oops对象压缩(实际上是pack了64位pointer成32位)的计算解决了这个问题。如果要使用这个功能需要增加参数:-XX:+UseCompressedOops。一个小提示:-XX参数配置时+-表示boolean值的true或false。
5JVM runtime environment包含很多功能:command line arguments解析, M life cycle, class loading, byte code interpreter, exception handling, synchronization, thread management, Java Native Interface, VM fatal error handling, and C++ (non-Java) heap management.
6. 命令行参数分三类:1)Standard command line options,它是符合JVM规范的那些options;2)Nonstandard command line options,它带有-X前缀;3)Developer command line options,它带有-XX前缀,这个配置有特定的系统需求,因此操作时需要有修改系统参数的权限。
7HotSpot VM的启动过程:
     1)解析命令行参数;2)Establish the Java heap sizes and the JIT compiler type;3)建立环境变量eg,LD_LIBRARY_PATH and CLASSPATH;4)设置Main-Class,如果没有配置就去jar的manifest文件中找;5)使用本地接口(JNI_CreateJavaVM)创建HotSpot VM并初始化;6)装载Main-Class;7)执行Main-Class。
8. JVM的退出前必须等待所有的非daemon线程都退出。
9. 类装载的三个阶段:loading, linking, and initializing,装载的时机:解析bytecode文件的过程中,解析constant pool symbol时就会触发类装载
10. Java6在同步方面的优化:
     1)偏向锁(Biased locking)——Java 6以前加锁操作都会导致一次原子CAS(Compare-And-Set)操作,CAS操作是比较耗时的,即使这个锁上实际上没有冲突,只被一个线程拥有,也会带来较大开销。为解决这一问题,Java 6中引入偏向锁技术,即一个锁偏向于第一个加锁的线程,该线程后续加锁操作不需要同步。大概的实现如下:一个锁最初为NEUTRAL状态,当第一个线程加锁时,将该锁的状态修改为BIASED,并记录线程ID,当这一线程进行后续加锁操作时,若发现状态是BIASED并且线程ID是当前线程ID,则只设置一下加锁标志,不需要进行CAS操作。其它线程若要加这个锁,需要使用CAS操作将状态替换为REVOKE,并等待加锁标志清零,以后该锁的状态就变成 DEFAULT,常用旧的算法处理。这一功能可用-XX:-UseBiasedLocking命令禁止。
     2)锁粗化(Lock coarsening)——如果一段代码经常性的加锁和解锁,在解锁与下次加锁之间又没干什么事情,则可以将多次加加锁解锁操作合并成一对。这一功能可用-XX:-EliminateLocks禁止。
     3)自适应自旋(Adaptive spinning)——一般在多CPU的机器上加锁实现都会包含一个短期的自旋过程。自旋的次数不太好决定,自旋少了会导致线程被挂起和上下文切换增加,自旋多了耗CPU。为此Java 6中引入自适应自旋技术,即根据一个锁最近自旋加锁成功概率动态调整自旋次数。 
11. 从JVM的视角来看有如下线程概念:1)New thread;2)Thread in Java,执行Java代码;3)Thread in vm,执行JVM代码;4)
Blocked thread——
acquiring a lock, waiting for a condition, sleeping, performing a blocking I/O operation等。
12. 从一些监控工具thread dumps, stack traces的视角看有如下线程概念:
     1)MONITOR_WAIT——A thread is waiting to acquire a contended monitor lock;
     2)CONDVAR_WAIT——A thread is waiting on an internal condition variable used by the HotSpot VM (not associated with any Java object);
     3)OBJECT_WAIT——A Java thread is performing a java.lang.Object.wait()call。           
13. HotSpot VM内部的一些线程:1)VM thread;2)Periodic task thread,又叫WatcherThread;3)Garbage collection threads;4)JIT compiler threads;5)Signal dispatcher thread。
14. 垃圾回收,采用generational garbage collector机制,live objects的生命周期过程为:Eden->From->To->Old具体情况如下:
     1)The young generation——刚产生的对象(不是所有,例如一些大对象可能就直接在老生代分配)被放入这个区域,这个区域经常发生minor garbage collections(效率很高,因为只扫描这一个区域),一般回收动作后的存活率很低。
     2)The old generation——如果在新生代经历几次minor回收后还能存活的对象就会被提拔到老生代,这个区域一般比新生代要大,并且回收(major garbage collections或叫full garbage collections)频率没有那么高。
     3)The permanent generation——严格的讲它不属于generation hierarchy,因为老生代中的对象不会被提拔到这个区域,它仅用于JVM自己维护一些元素据,例如class data structures, interned strings等。如下图所示:
15. 在新生代做minor回收时,如何在不扫描整个老生代的情况下找出所有live objects?方法是采用一个叫做card table的数据结构,The old generation is split into 512-byte chunks called cardsThe card table is an array with one byte entry per card in the heap. 每次更新老生代对象的reference field时,同时must also ensure that the card containing the updated reference field is marked dirty by setting its entry in the card table to the appropriate value。简单的来讲就是如果要更新一个对象的引用域那么也要把引用域所在对象的card标记为“脏”状态,以后minor回收时只会扫描这些标有“脏”的card。如下图所示:
16. The young generation(采用coping算法实现回收),当有新对象需要分配空间时,如果发现空间不足就会触发minor回收,它又分为:
     1)The eden,大多数新对象在这里分配(不是所有,例如一些大对象可能就直接在老生代分配),这个区域当执行一次minor回收后几乎没有生存者。
     2)The two survivor spaces,两个survivor(from和to)空间中有一个用来保存对象,而另一个是空的,用来在下次的新生代GC中保存对象。
17. 垃圾回收器的类型
     1)The Serial GC,单虚拟处理器处理回收工作,使用stop-the-world garbage collection model,因此有延时,最后得到的free空间是连续的。适应场景:没有低延时的需求;client-style的应用;一台机器上面运行N个JVM。缺点:有延时,效率不高。
     2)The Parallel GC(提高吞吐量),充分利用多处理器,使用stop-the-world garbage collection model,因此有延时,使用和顺序GC相同的新老生代对象处理方式。最后得到的free空间是连续的。适应场景:高吞吐量需求;server-style的应用;batch processing engines, scientific computing。优点:高吞吐量;缺点:有延时。
     3)The Mostly-Concurrent GC—CMS(减少延时),新生代对象的处理方式和并行GC一致,但是老生代采用并发处理方式,仅有两次较小的延时,但是它是用新生代的延时来换取老生代的快速操作。原理为:先进行一个initial mark暂停阶段用于mark一个从老生代外部可直接reachable的集合;然后经历一个concurrent marking 阶段用于并发mark一个从上一个集合可达的对象集合;由于这个阶段执行时并没有暂停应用程序,所以有可能在这个时期对象发生了修改。为了解决这个问题,引入了remark暂停阶段,用于重新mark修改过的对象;事实上,为了提高remark的效率,在执行remark前还优化出了一个pre-cleaning阶段;所有对象都被mark完成后,最后执行concurrent sweeping phase操作,用于收集free空间。优点:低延时;缺点:因为它得到的free空间是一个不连续的链表,所以在分配上没有连续空间那么高效,这个也会间接的影响到minor回收的效率,因为minor操作后总是伴随着老生代空间的分配。适用场景:快速响应:data-tracking servers, Web servers等。补充一点:目前的新版本JVM已经进行了一些优化,实现了前面两个并发操作的并发执行(marking and sweeping),比如使用-XX:+CMSParallelRemarkEnabled 降低标记停顿
     4)The Garbage-First GC(将来可能替代CMS),它是一个parallel, concurrent, and incrementally compacting low-pause garbage collector。
18. 这里提一下并发和并行的概念,方便理解并行GC和并发GC
     1)并发,是在同一个cpu上同时(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)运行多个程序。所有的并发处理都有排队等候,唤醒,执行至少三个这样的步骤。
     2)并行,是每个cpu运行一个程序,强调同一时刻同时执行。
19. mark完成后如何整理free空间,方法有两种:1)将free空间推倒一头,另外一头是活动对象,这种方式叫做标记—清除—压缩(compact)(顺序GC和并行GC);2)空间不做移动,而是用链表把它们连接起来,这种方法叫做标记—清除(并发GC,但是现在也可以通过-XX+UseCMSCompactAtFullCollection保证在full收集时进行内存压缩);3)这种就是新生代中使用的标记—清除—复制。
19. 垃圾回收的触发点
     1)minor garbage collection发生分配空间时eden已经满的情况,它只负责新生代的垃圾回收,一次收集完成后eden区的空间被清空,活对象被移入survivor to区域,survivor from也为空,其对象也被移到survivor to
     2)如果老生代存活对象占住的空间大于某个阀值就会启动CMS cycle,这个阀值可以使用-XX:CMSInitiatingOccupancyFraction=80来设置;
     3)full GC在如下四种情况会被触发,收集完成之后,新生代的空间被清空,老生代和持久代的空间被压缩。
          3.1)旧生代空间不足。旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space。
          3.2)Permanet Generation空间满。Permanet Generation中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space。为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
          3.3)CMS GC时出现promotion failed和concurrent mode failure。 对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的,这里可以使用-XX:CMSInitiatingOccupancyFraction=80来保证老生代free空间;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的,它会导致暂停所有Java应用来收集垃圾并压缩内存。
应对措施为:增大survivor space、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
         3.4)统计得到的Minor GC晋升到老生代的平均大小大于生代的剩余空间。在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到生代的平均大小大于生代的剩余空间,那么就直接触发Full GC。
除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
20. 对象分配规则
1.对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
2.大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
3.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
4.动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
5.空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。