一、背景
线上环境下,jvm经常发生full gc;运维监控方面,服务健康检测不健康;用户体验方面,经常出现大量的慢接口调用。
经观察,这三者是伴随发生的。起初,我们从慢接口入手,看到接口中的慢主要是在访问数据存储层的时候,但是去数据库却没有看到任何的慢查询日志(包括mongodb和redis);但是能解释为什么服务不健康,服务假死了,不再能够消费任何请求。
所以,我们就先解决导致接口慢的原因(gc)。
二、业务特点
业务上的接口 ,慢大多数都是查询类的 ,会生成大量jvm对象在堆内存中。
先是在年轻代,由young gc回收,如果年轻代的内存不够,则会直接分配到年老代。
年老代的内存大小有限,要回收他们,就只有full gc了。
当然,在年轻代的对象,要想回收,和对象的年龄也有关。
所以,我们的初步思路是增大年轻代的内存,让对象尽可能在young gc就回收了。
目标
- 年轻代的对象回收,呈现锯齿形。
- 最终是为了减少full gc
三、jdk8的默认垃圾回收器ParallelGC
Parallel Scavenge + Parallel Old
- java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=67108864 -XX:MaxHeapSize=1073741824 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
实际运行的jvm参数是
java -Xms2800m -Xmx2800m -XX:SurvivorRatio=18 -XX:MaxTenuringThreshold=15 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError -jar /opt/xx/xxx.jar
参考地址:
MaxTenuringThreshold 年龄大小,默认15
SurvivorRatio 默认8,Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10。(它和-Xmn大小有关)
四、jvm体系结构
五、目标
1、降低 Minor GC 频率
通常情况下,由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间来降低 Minor GC 的频率。
可能你会有这样的疑问,扩容 Eden 区虽然可以减少 Minor GC 的次数,但不会增加单次 Minor GC 的时间吗?
我们知道,单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对象)。
假设一个对象在 Eden 区的存活时间为 500ms,Minor GC 的时间间隔是 300ms,那么正常情况下,Minor GC 的时间为:T1+T2。
当我们增大新生代空间,Minor GC 的时间间隔可能会扩大到 600ms,此时一个存活 500ms 的对象就会在 Eden 区中被回收掉,此时就不存在复制存活对象了,
所以再发生 Minor GC 的时间为:两次扫描新生代,即 2T1。可见,扩容后,Minor GC 时增加了 T1,但省去了 T2 的时间。
通常在虚拟机中,复制对象的成本要远高于扫描成本。如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加 Minor GC 的时间。
如果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。
因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。
-Xmn1500m, 之前的年轻代就500m~700m。
2、降低 Full GC 的频率
通常情况下,由于堆内存空间不足或老年代对象太多,会触发 Full GC,频繁的 Full GC 会带来上下文切换,增加系统的性能开销。
我们可以使用哪些方法来降低 Full GC 的频率呢?
减少创建大对象:在平常的业务场景中,我们习惯一次性从数据库中查询出一个大对象用于 web 端显示。 例如,我之前碰到过一个一次性查询出 60 个字段的业务操作,这种大对象如果超过年轻代最大对象阈值,会被直接创建在老年代;
即使被创建在了年轻代,由于年轻代的内存空间有限,通过 Minor GC 之后也会进入到老年代。 这种大对象很容易产生较多的 Full
GC。我们可以将这种大对象拆解出来,首次只查询一些比较重要的字段,如果还需要其它字段辅助查看,再通过第二次查询显示剩余的字段。
增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低 Full GC 的频率。
把jvm堆内存由2800m --> 3800m, 且两者一样大 -Xms3800m -Xmx3800m
3、垃圾回收器为CMS
-XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
新生代使用ParNew,老年代使用CMS-XX:+CMSParallelRemarkEnabled
降低标记停顿-XX:CMSInitiatingOccupancyFraction=92
参考:
六、调整后的效果对比
1、full gc 频率显著减少
2、慢接口不再剧增
3、年轻代内存增大
-
调整前
-
调整后
年轻代的内存呈现锯齿形,说明年轻代的对象得到了及时回收。
4、老年代的内存增速显著变慢
七、总结
1、AdaptiveSizePolicy
idea在配置启动参数-XX:SurvivorRatio时不生效的问题,是因为JDK 1.8 默认使用 UseParallelGC 垃圾回收器,该垃圾回收器默认启动了 AdaptiveSizePolicy(自适应大小策略),会根据GC的情况自动计算计算 Eden、From 和 To 区的大小;要使-XX:SurvivorRatio生效,就需要关闭该策略 -XX:-UseAdaptiveSizePolicy。
2、使用CMS时,会默认停用AdaptiveSizePolicy开关
/ # jinfo -flag UseAdaptiveSizePolicy 8
-XX:-UseAdaptiveSizePolicy
验证SurvivorRatio的设置是否生效
jinfo -flag SurvivorRatio 8
-XX:SurvivorRatio=18
验证是否启用了CMS垃圾回收器
jinfo -flag UseConcMarkSweepGC 8
-XX:+UseConcMarkSweepGC