JVM GC耗时频频升高,我来教你排查

1. 背景

多个业务线的应用出现LongGC告警程序员

最近一段时间,常常收到CAT报出来的Long GC告警(配置为大于3秒的为Longgc)。面试

2. 知识回顾

2.1 JVM堆内存划分

  • 新生代(Young Generation)

新生代内被划分为三个区:Eden,from survivor,to survivor。大多数对象在新生代被建立。Minor GC针对的是新生代的垃圾回收。算法

  • 老年代(Old Generation)

在新生代中经历了几回Minor GC仍然存活的对象,就会被放到老年代。Major GC针对的是老年代的垃圾回收。本文重点分析的CMS就是一种针对老年代的垃圾回收算法。另外Full GC是针对整堆(包括新生代和老年代)作垃圾回收的。设计模式

  • 永久代(Perm)

主要存放已被虚拟机加载的类信息,常量,静态变量等数据。该区域对垃圾回收的影响不大,本文不会过多涉及。网络

2.2 CMS垃圾回收的6个重要阶段

一、initial-mark 初始标记(CMS的第一个STW阶段),标记GC Root直接引用的对象,GC Root直接引用的对象很少,因此很快。多线程

二、concurrent-mark 并发标记阶段,由第一阶段标记过的对象出发,全部可达的对象都在本阶段标记。并发

三、concurrent-preclean 并发预清理阶段,也是一个并发执行的阶段。在本阶段,会查找前一阶段执行过程当中,重新生代晋升或新分配或被更新的对象。经过并发地从新扫描这些对象,预清理阶段能够减小下一个stop-the-world 从新标记阶段的工做量。工具

四、concurrent-abortable-preclean 并发可停止的预清理阶段。这个阶段其实跟上一个阶段作的东西同样,也是为了减小下一个STW从新标记阶段的工做量。增长这一阶段是为了让咱们能够控制这个阶段的结束时机,好比扫描多长时间(默认5秒)或者Eden区使用占比达到指望比例(默认50%)就结束本阶段。优化

五、remark 重标记阶段(CMS的第二个STW阶段),暂停全部用户线程,从GC Root开始从新扫描整堆,标记存活的对象。须要注意的是,虽然CMS只回收老年代的垃圾对象,可是这个阶段依然须要扫描新生代,由于不少GC Root都在新生代,而这些GC Root指向的对象又在老年代,这称为“跨代引用”。this

六、concurrent-sweep ,并发清理。

3. 分析

下面先看看出现LongGC时发生了什么。

选取其中一个应用分析其GC日志,发现LongGC发生在CMS 的收集阶段。

箭头1 显示abortable-preclean阶段耗时4.04秒。箭头2 显示的是remark阶段,耗时0.11秒。

虽然abortable-preclean阶段是concurrent的,不会暂停其余的用户线程。就算不优化,可能影响也不大。可是每天收到各个业务线的gc报警,长久来讲也不是好事。

在调优以前先看下该应用的GC统计数据,包括GC次数,耗时:

统计期间内(18天)发生CMS GC 69次,其中 abortable preclean阶段平均耗时2.45秒,final remark阶段平均112ms,最大耗时170ms.

4. 优化目标

下降abortable preclean 时间,并且不增长final remark的时间(由于remark是STW的)。

5. JVM参数调优

5.1 第一次调优

先尝试调低abortable preclean阶段的时间,看看效果。

有两个参数能够控制这个阶段什么时候结束:

  • -XX:CMSMaxAbortablePrecleanTime=5000

默认值5s,表明该阶段最大的持续时间

  • -XX:CMSScheduleRemarkEdenPenetration=50

默认值50%,表明Eden区使用比例超过50%就结束该阶段进入remark

调整为最大持续时间为1s,Eden区使用占比10%,以下:

-XX:CMSMaxAbortablePrecleanTime=1000

-XX:CMSScheduleRemarkEdenPenetration=10

为何调整成这样两个值,咱们是这样考虑的:首先每次CMS都发生在老年代使用占比达到80%时,由于这是由下面两个参数决定的:

-XX:CMSInitiatingOccupancyFraction=80

-XX:+UseCMSInitiatingOccupancyOnly

而老年代的增加是因为部分对象在Minor GC后仍然存活,被晋升到老年代,致使老年代使用占比增加的,也就是在每次CMS GC发生以前刚刚发生过一次Minor GC,因此在那一刻新生代的使用占比是很低的。那么咱们预计这个时候尽快结束abortable preclean阶段,在remark时就不须要扫描太多的Eden区对象,remark STW的时间也就不会太长。

调整的思路是这样了,那到底效果如何呢?

第一次调整的的结果

在统计期间(17小时左右)内,发生过2次CMS GC。Abortable Preclean 平均耗时835ms,这是预期内的。可是Final Remark 平均耗时495ms(调整前是112ms),其中一次是80ms,另外一次是910ms!将近1秒钟!Remark是STW的!对于要求低延时的应用来讲这是没法接受的!

对比这两次CMS GC的详细GC日志,咱们发现了一些对分析问题很是有用的东西。

remark耗时80ms的那次GC日志

[YG occupancy: 181274 K (1887488 K)] - 年轻代当前占用状况和总容量

耗时80ms的此次remark发生时(早上9点,非高峰时段),新生代(YG)占用181.274M。

remark耗时910ms的那次GC日志

[YG occupancy: 773427 K (1887488 K)]

耗时910ms的此次remark发生时(晚上10点左右,高峰时段),新生代(YG)占用773.427M。由于这个时候高峰期,新生代的占用量上升的很是快,几乎一样的时间内,非高峰时段仅上升到181M,可是高峰时段就上升到773M。

这里能得出一个有用的结论:若是abortale preclean阶段时间过短,随后在remark时,新生代占用越大,则remark持续的时间(STW)越长。

这就陷入了两难了,不缩短abortale preclean耗时会报longgc;缩短的话,remark阶段又会变长,并且是STW,更不能接受。

对于这种状况,CMS提供了CMSScavengeBeforeRemark参数,尝试在remark阶段以前进行一次Minor GC,以下降新生代的占用。

-XX:+CMSScavengeBeforeRemark

Enables scavenging attempts before the CMS remark step. By default, this option is disabled.

5.2 第二次调优

调优前的考虑:

增长-XX:+CMSScavengeBeforeRemark 不是没有代价的,由于这会增长一次Minor GC停顿。因此这个方案好或者很差的判断标准就是:增长CMSScavengeBeforeRemark参数以后的minor GC停顿时间 + remark 停顿时间若是比增长以前的remark GC停顿时间要小,这才是好的方案。

第二次调整的结果

在统计期间(20小时左右)内,发生3次CMS GC。Abortable preclean 平均耗时693ms。Final remark平均耗时50ms,最大耗时60ms。Final remark的时间比调优前的平均时间(112ms)更低。

那么CMS GC前的Minor GC停顿时间又如何呢?来看看详细的GC日志。

3次CMS GC remark前的Minor GC日志分析

第1次是非高峰时段的表现,Minor GC 耗时 0.01s + remark耗时 0.06s = 0.07s = 70ms,以下

第2次是高峰时段,Minor GC 耗时 0.01s + remark耗时 0.05s = 0.06s = 60ms,以下

第3次是非高峰时段,Minor GC 耗时 0.00s + remark耗时 0.04s = 0.04s = 40ms,以下

因此,3次Minor GC + remark耗时的平均耗时 < 60ms,这比第一次调优时remark平均耗时495ms好得多了。

6.优化结果

至此,咱们最初的目标- 下降abortable preclean 时间,并且不增长final remark的时间 ,已经达到了。甚至remark的时间也缩短了。

7. 小结

解决abortable preclean 时间过长的方案能够归结为两步:

  • 缩短abortable preclean 时长,经过调整这两个参数:

-XX:CMSMaxAbortablePrecleanTime=xxx

-XX:CMSScheduleRemarkEdenPenetration=xxx

调整为多少的一个判断标准是:abortable preclean阶段结束时,新生代的空间占用不能大于某个参考值。 在前面第一次调优后,新生代(YG)占用181.274M,remark耗时80ms;新生代(YG)占用773.427M时,remark耗时910ms。因此这个参考值能够是300M。而若是新生代增加过快,像此次调优应用2秒内就能用光2G新生代堆空间的,就只能经过CMSScavengeBeforeRemark作一次Minor GC了。

  • 增长CMSScavengeBeforeRemark参数开启remark前进行Minor GC的尝试

虽然官方说明这个增长这个参数是尝试进行Minor GC,不必定会进行。但实际使用起来,几乎每次remark前都会Minor GC。

8. 总结

  1. 调优前明确目标

  2. 调优过程对GC指标进行数据统计分析(本文借助gceasy.io在线分析工具)来验证效果

  3. 须要能看懂GC日志

  4. GC调优不是一个一蹴而就的事情,它是微调-观察-再微调的过程。因此须要比较深刻了解GC的一些基础,才能少走弯路。

小编总结了2020面试题,这份面试题的包含的模块分为19个模块,分别是: Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM 。

关注公众号:程序员白楠楠,获取上述资料。

 
相关文章
相关标签/搜索