在某应用的性能测试时,当并发增加一定程度TPS出现下降且波动很大,但系统并没有出现明显的资源使用瓶颈。查看GC日志发现5min内Full GC次数达到43次,平均FullGC时间为1s。
![一次JVM调优 - 网易杭州QA - 网易杭州 QA Team](http://img0.ph.126.net/gpj2yEi1kvJFavA8B9ZWTQ==/1421730107466350091.png)
为了定位问题,增加-XX:+PrintTenuringDistribution参数输出对象age的大小分布 使用ParNew收集器时才会生效) ,同时使用VisualVM工具观察其垃圾回收的过程。可以看出,Young区的对象很快进入了Old区,首先想到的解决方法是增加Young区大小,使得对象在Young区存活时间更长,尽量通过YoungGC回收掉。因此将原来4G的JVM内存增加到8G,在相同的并发下,Full GC 次数减少到6次,平均Full GC时间1s,性能有所好转。
![一次JVM调优 - 网易杭州QA - 网易杭州 QA Team](http://img2.ph.126.net/CYu6iQGq52peaL_5e4B51A==/6608656420189758257.png)
将JVM增加到8G后,观察到Young区对象的Tenuring Threshold仍然为1,且这些对象并没有达到Max Tenuring Threshold(默认是15)就晋升到了老年代。
![一次JVM调优 - 网易杭州QA - 网易杭州 QA Team](http://img0.ph.126.net/hMuvtC5-1pCSjeX7xwxqAw==/3028389274530289938.png)
通过复习JVM的内存分配原则,才恍然大悟。其中一条是这样的:
- 动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到Max Tenuring Threshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到Max Tenuring Threshold中要求的年龄。
有了理论依据,这下调节起来就有的放矢了。在保持JVM内存8G不变的情况下,同时增加参数-XX:SurvivorRatio=2,将survivor区大小为1.5G左右,再通过VisualVM查看Tenuring Threshold=1的对象所占比例已远远减小,如下图所示,这就避免了Young区对象过早进入Old区。
![一次JVM调优 - 网易杭州QA - 网易杭州 QA Team](http://img1.ph.126.net/auZ07Kn5zTSNeYhSaMxeew==/6608234207725761115.png)
分析GC日志得到Full GC次数为0,Young GC次数为102次,平均YoungGC时间为0.1S,TPS增加明显,且波动较小,性能提升明显。不同JVM参数配置下的性能表现如下:
JVM参数 | Full GC次 | Full GC Time | Young GC次数 | Young GC Time | TPS |
JVM 4g Young区2g Survivor区 256M Old区2g | 43 | 0.7s | 220 | 0.07s | 631 |
JVM 8g Young区2g Survivor区 512M Old区5g | 6 | 1s | 118 | 0.08s | 673 |
JVM 8g Young区3g Survivor区 1.5g Old区2g | 0 | - | 102 | 0.1s | 808 |
最后在回顾下JVM的内存分配原则和分代GC原则,理解而熟记以便不时之需。
JVM内存分配原则
- 对象优先在Eden分配
Eden空间不足时发起一次Minor GC, -XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志
- 大对象直接进入老年代
虚拟机提供了-XX:PretenureSizeThreshold,大于这个设置值的对象直接在老年代中分配 。只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数
- 长期存活的对象将进入老年代
对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度 (默认为15)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过 参数-XX:MaxTenuringThreshold来设置。
- 动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
JVM分代垃圾回收原则
MinorGC
- 当创建对象时Eden区剩余空间不足,触发MinorGC,进行年轻代( Eden 、1个survivor区)垃圾回收。
- 默认情况下Full GC会触发Minor GC,在PS GC时可通过设置-XX:-ScavengeBeforeFullGC来禁止触发Minor GC。
OldGC
- 老生代空间不足 。
- 永久代Permanet Generation空间满 。
- 晋升失败 。Promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成
- 空间分配担保 。每次Minor GC时,JVM会计算Survivor区移至老年区的对象平均大小,如果这个值大于老年区的剩余空间则进行一次Full GC,否则将检查HandlePromotionFailure设置,若设为true,则只进行MinorGC,反之进行Full GC。
- System.gc()被显式调用
最后的最后,JVM理论一定要结合实践和业务对象的特点,没有最优的参数,只有适合业务对象特点的相对最优参数。对于JVM参数调优的一点小心得:
首先,对于可能会有GC问题的应用,尽可能详细地收集JVM GC的信息,GC日志分析和VisualVM工具搭配使用 。
其次,结合业务对象的特点和JVM内存分配原则和GC原则,找准问题所在。
最后,使用合适的JVM参数进行调节,验证其效果。