通过 gc.log 调优 JVM

在 Java 的项目中,需要查看 GC 信息来判断系统是否正常运行,很多低延迟高可用 Java 服务的系统可用性经常受 GC 停顿的困扰。

GC 停顿指垃圾回收期间 STW(Stop The World),当 STW 时,所有应用线程停止活动,等待GC停顿结束,若停顿时间过长,容易造成这段时间内接口响应时间上升,甚至超时。

一、GC 日志查看

GC 日志默认是关闭的,需要查看 GC 日志首先需要开启 GC 日志。

常用 GC 日志的配置有如下,组合以下命令,在启动时加入,就可以得到 GC 日志。

-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2020-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-XX:GCLogFileSize 日志大小
-Xloggc:../logs/gc.log 日志文件的输出路径

例如使用如下命令启动

$ java -XX:+PrintGCDateStamps -verbose:gc -XX:+PrintGCDetails -Xloggc:../gc.log -XX:+UseGCLogFileRotation -jar demo.jar 

当项目启动,获取到 gc.log 后,可以使用 GCeasy 上传 gc.log 文件,进行分析。

二、调整新生代/老年代比例

分析的结果中注意到一点:新生代分配 1G,使用峰值为 0.93G,使用率约 90%;老年代分配 2.1G,使用峰值为 1.7G,使用率约 80%。也就是说老年代有部分空间没用到,能否提高利用率?

JVM memory size

堆的内存模型大致为 新生代/老年代 = 1/2,能否通过增大新生代,减小老年代的方法来提高内存利用率?

JVM 内存模型

例如把新生代、老年代比例设置为 1:1,设置参数:-XX:NewRatio=1

-XX:NewRatio 命令是设置 Old 与 Yong 的比例,比如值为 2,也就是默认值,即 Old Generation : Yong Generation = 2,注意:该参数的值只能时正整数,无法使用小数,例如设置 NewRatio=0.5,是无法启动的。

NewRatio=0.5

运行一段时间后,再看 JVM 内存使用

JVM memory size

发现内存使用率基本没有变化,但是,停顿时间有了明显的变化。系统运行了 6 天,调整前总停顿时间约 5 min 30 sec。

调整前总停顿时间

调整后总停顿时间约 2 min 50 sec,相较于调整前,停顿时间下降约 50%。

调整后总停顿时间

调整前,平均停顿时间 20ms,最大停顿时间 180ms。

调整前平均时间

调整后,平均停顿时间 16ms,相较于调整前停顿时间下降约 20%,最大停顿时间 140ms,下降约 22%。

调整后平均时间

分析原因,主要是 Full GC 次数减少。调整前 FGC 次数为 6 次。

调整前 FGC

调整后 FGC 次数还不到 3 次,相较于调整前,FGC 次数下降约 50%,而 FGC 一定会 Stop The World,FGC 次数减少,停顿时间自然会减少。

调整后 FGC

为什么 FGC 次数会减少呢?

因为从 年轻代 晋升到 老年代 对象的数量减少了。调整前晋升数量约 5MB。

调整前晋升数量

而调整后晋升数量不到 2MB。

调整后晋升数量

因为年轻代增大了,可以容纳更多的对象,更多垃圾在年轻代就被回收了,所以导致晋升到老年代的对象减少,老年代对象减少导致 FGC 次数也减少,所以停顿时间也随之下降。

这里引出一个问题:年轻代调大后,YoungGC 时间是否会增加?

答案是不会,应为 YGC 主要采用复制算法,耗时也集中在复制过程中,虽然年轻代增大了,但每次复制时,存活的对象数量大致不会改变,所以耗时并不会增加。

调整前 YGC 停顿时间集中在 10~40ms 之间。

调整前 YGC 停顿时间

调整后 YGC 停顿时间基本不变,也是集中在 10~40ms 之间,并且可以看到调整后 YGC 的最大停顿时间还下降了。

调整后 YGC 停顿时间

是否还能继续优化?

三、调整晋升次数

过早晋升问题

比如说 Old 区触发的回收阈值是 80%,经历过一次 GC 之后下降到了 10%,这就说明 Old 区的 70% 的对象存活时间其实很短,Old 区大小每次 GC 后从 1.3 G 回收到 0.2G,也就是说回收掉了 1.1G 的垃圾,只有 200M 的活跃对象。整个 Heap 目前是 4G,活跃对象只占了不到二十分之一。

调整后 FGC

主要原因:

1、Young/Eden 区过小: 过小的直接后果就是 Eden 被装满的时间变短,本应该回收的对象参与了 GC 并晋升,Young GC 采用的是复制算法,copying 耗时远大于 mark,也就是 Young GC 耗时本质上就是 copy 的时间,没来及回收的对象增大了回收的代价,所以 Young GC 时间增加,同时又无法快速释放空间,Young GC 次数也跟着增加。

2、对象太容易晋升。

解决办法:

对于第1点,上述已通过调大年轻代来解决。

对于第2点,可以继续优化,通过设置 -XX:MaxTenuringThreshold 来设置年轻代晋升老年代的最大年龄阈值。需要注意的是 -XX:MaxTenuringThreshold 设置的是最大阈值,也就是说有些对象没有达到阈值也会进入老年代,例如年轻代放不下的大对象。

所有的对象都在 Eden 区创建,当 Eden 区满了,那么就会触发一次 Young GC。少量有用的对象会复制到 From 区,年龄+1。

当 Eden 区再次被用完,就再触发一次 YoungGC,这个时候跟刚才稍稍有点区别。这次触发 Young GC 后,会将 Eden 区与 From 区还在被使用的对象复制到 To 区,年龄+1。

经过若干次 YoungGC 后,有些对象在 From 与 To 之间来回游荡,每游荡一次,对象的年龄+1,当年龄达到 From 区与 To 区的底线(-XX:MaxTenuringThreshold 设置的阈值)的时候,这些家伙要是到现在还没挂掉,一起复制老年代吧。

MaxTenuringThreshold 的取值范围是 1~15,默认值是15,但 CMS 收集器 MaxTenuringThreshold 的默认值是 6,但是官方文档并没有说明,为何 CMS 特殊是 6。因此若使用 CMS 收集器时,可以考虑增大 MaxTenuringThreshold 的值。

你可能感兴趣的:(通过 gc.log 调优 JVM)