JVM性能优化(调优)

JVM做为通用的优化手段,时常会受到一部分人的青睐,从几个简单的JVM优化方案入手,探寻JVM性能优化问题。

先简述JVM优化所涉及到的一些概念,再看三个简单的JVM优化方案。

垃圾回收器

不同的垃圾回收器,适用于不同的场景。常见的垃圾回收器:

串行(Serial)

回收器是单线程的回收器,简单、易实现、效率高。

并行(ParNew)

Serial的多线程版本,充分的利用CPU资源,减少GC时间。

吞吐量优先(Parallel Scavenge)

侧重点吞吐量。

并发标记清除(CMS,Concurrent Mark Sweep)

以获取较短停顿时间为目标,基于“标记-清除”算法实现,常用于年老代的收集器,也就下文所说的CMS收集器。

通用参数

对象活跃数据的大小,是指应用程序稳定运行时,长期存活对象在堆中的占比,也就是Full GC后,堆中年老代占用空间的大小。可以在GC日志中,Full GC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小。

活跃数据和各分区之间的比例关系如下:

空间/倍数

总大小

3-4 倍活跃数据的大小

新生代 

1-1.5 活跃数据的大小

老年代 

2-3 倍活跃数据的大小

永久代 

1.2-1.5 倍Full GC后的永久代空间占用

明确jvm优化目标

明确应用程序的系统需求,是性能优化的基础,系统的需求,是指应用程序运行时某方面的要求,譬如:高可用、低延迟、高吞吐、以高可用和低延迟作为优化目标。

Minor GC和Major GC频繁

通过加大新生代空间来降低Minor GC的频率,因为加大内存直接导致GC间隔被延长,如果一个对象的生命周期,能在下一个GC到来之前就结束,GC过程中需要搬运的存活对象数量就会相对减少。

因此新生代大小的参数,可以结合应用中对象的生命周期特征,以及预期的最大延迟时间,进行合理设置。如果应用中存在大量生命周期较短的对象,合理加大新生代空间,以影响年老代对象的数量,Major GC频率自然得到改善。如果存在较多的长生命周期的对象,需要提高年老代空间。

新生代对象经历多少次Minor GC后会晋升到年老代?

或许MaxTenuringThreshold参数能给出答案,但其实不然,Hotspot会动态计算这个晋升阀值。如果新生代中的对象,经历若干次Minor GC后,此时,新生代中survivor空间就被消耗掉50%,则Hotspot就会从新生代对象中,挑出年龄最小的值做为晋升的阀值。

举例:一个对象在经历了5次Minor GC后,survivor空间就被应用创建的众多对象消耗掉50%,而这时survivor空间中的对象最少都经历5次或以上的Minor GC,那5就成为了晋升阀值。

Major GC时间过长

Major GC默认使用CMS收集器,CMS收集器GC分为四个阶段:

1、Init-mark初始标记(STW)

该阶段进行可达性分析,标记GC ROOT能直接关联到的对象,所以很快。

2、Concurrent-mark并发标记

由前阶段标记过的对象出发,所有可到达的对象都在本阶段中标记。

3、Remark重标记(STW)

需要暂停应用程序,重新扫描堆中的对象,进行可达性分析,标记活着的对象。并发标记阶段应用线程并没停止,对象的生命状态会有所变化,这个阶段是以新生代中的对象为根,来进行对象存活判断。

4、并发清理

进行并发的垃圾清理工作。

以上四个阶段中第三阶段的Remark重标记,最影响应用程序的响应时间,因为”跨代引用“问题,这个阶段需要进行全“堆”扫描。为了减少Remark阶段中垃圾对象的数量,在执行Remark前,会进行一次可中断的Minor GC,而Minor GC的开始取决于Eden区的使用情况,默认Eden区使用超过2M时,会启动Minor GC。

为了防止Remark阶段前的Minor GC迟迟不肯到来,导致Remark不能进行,默认为5s预清理时间一到,不管有没有发生Minor GC,都会中止此阶段,直接进入Remark阶段。

CMSMaxAbortablePrecleanTime用于设置预清理的时间。对于这种没等到Minor GC就匆匆开始标记的情况,CMS提供CMSScavengeBeforeRemark参数,用于保证Remark前强制进行一次Minor GC。这也是调整Major GC时间过长的关键所在。

相信了解JVM都知道card table,这个就是用来防止年老代,持有新生代对象引用时,导致Minor GC扫描全堆,通过使用card table来记录这一情况,避免进行全堆扫描。

频繁Full GC

有2情况会触发Full GC,在Full GC时,应用程序会被冻结。

concurrent-mode-failure:

当CMS收集垃圾过程中,有新的对象要进入年老代,但是年老代空间不足。

promotion-failed:

当年轻代进行GC时,有大量对象存活,但是两个交换区不能容纳,只能晋升到年老代,但此时年老代空间已无法容纳。Full GC最直接的原因来自年老代空间不足,而引发年老代空间不足的情况,通常来自大量对象晋升到年老代。当年老代空间达到80%,CMS收集器进行处理时,又有大量新的对象晋升到年老代,可能会出现concurrent mode failure引发Full GC。

通过调整-XX:CMSInitiatingOccupancyFraction=50,在年老代空间达到50%时就开始CMS收集,来缓决CMS收集过程中大量失败的问题。

通过调整-XX:CMSMaxAbortablePrecleanTime=500,来减少预清理阶段暂停时间过长的问题。

考虑到CMS收集器不会进行内存整理(compact),因此加入-XX:+UseCMSCompactAtFullCollection参数,来开启内存整理。并通过-XX:CMSFullGCsBeforeCompaction=4参数来控制内存整理频率(示例参数为Full GC4次后会进行compact)。

如果,以上参数会够立竿见影,但一段时间后又出现这种情况,就要考虑是否需要加大年老代空间了。当然适当的调小eden区,可以缓解CMS在收集过程中,年老代拥入大量新对象。

如果设置了-XX:+UseCMSInitiatingOccupancyOnly参数,只有当年老代占用确实达到了,-XX:CMSInitiatingOccupancyFraction参数所设定的比例时,才会触发CMS进行收集。

如果没有设置-XX:+UseCMSInitiatingOccupancyOnly参数,那么JVM系统会根据统计数据,自行决定什么时候触发CMS收集,因此有时会遇到设置了80%的参数,但是50%时就已经触发了,大多是因为这个参数没有设置的原因。因此可以看出,所有的JVM优化并不是一边倒的策略,总是相互影响互相牵制。

以上所有都是针对JDK9以下的默认JVM,JDK9默认使用G1收集器,在JVM性能与调优方面大为不同。