写点什么

☕【JVM 性能调优】「CMS 垃圾回收器」调优化方案

发布于: 2021 年 06 月 18 日
☕【JVM性能调优】「CMS垃圾回收器」调优化方案

前提概要


如果没有冬天,春天不会如此悦人;如果没有偶尔的不幸,幸运不会如此受人欢迎

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


  1. initial-mark 初始标记(CMS 的第一个 STW 阶段),标记 GC Root 直接引用的对象,GC Root 直接引用的对象不多,所以很快。

  2. concurrent-mark 并发标记阶段,由第一阶段标记过的对象出发,所有可达的对象都在本阶段标记

  3. concurrent-preclean 并发预清理阶段,也是一个并发执行的阶段。在本阶段,会查找前一阶段执行过程中,[从新生代晋升或新分配或被更新的对象]。通过并发地重新扫描这些对象,预清理阶段可以减少下一个 stop-the-world 重新标记阶段的工作量

  4. concurrent-abortable-preclean并发可中止的预清理阶段。这个阶段其实跟上一个阶段做的东西一样,也是为了减少下一个 STW 重新标记阶段的工作量。增加这一阶段是为了让我们可以控制这个阶段的结束时机,比如扫描多长时间(默认 5 秒)或者 Eden 区使用占比达到期望比例(默认 50%)就结束本阶段

  5. remark 重标记阶段(CMS 的第二个 STW 阶段),暂停所有用户线程,从 GC Root 开始重新扫描整堆,标记存活的对象。需要注意的是,虽然 CMS 只回收老年代的垃圾对象,但是这个阶段依然需要扫描新生代,因为很多 GC Root 都在新生代,而这些 GC Root 指向的对象又在老年代,这称为“跨代引用”

  6. concurrent-sweep ,并发清理

分析

分析其 GC 日志,发现 GC 发生在 CMS 的收集阶段



  • 箭头 1 显示 abortable-preclean 阶段耗时 4.04 秒

  • 箭头 2 显示的是 remark 阶段,耗时 0.11 秒

  • 虽然 abortable-preclean 阶段是 concurrent 的,不会暂停其他的用户线程。就算不优化,可能影响也不大。


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



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

优化目标

降低 abortable preclean 时间,而且不增加 final remark 的时间(因为 remark 是 STW 的)。

JVM 参数调优

第一次调优

先尝试调低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
复制代码

这两个设置一般配合使用,一般用于『降低 CMS GC 频率或者增加频率、减少 GC 时长』的需求

  • -XX:CMSInitiatingOccupancyFraction=80 是指设定 CMS 在对内存占用率达到 80%的时候开始 GC(因为 CMS 会有浮动垃圾,所以一般都较早启动 GC);

  • -XX:+UseCMSInitiatingOccupancyOnly标志来命令 JVM 不基于运行时收集的数据来启动 CMS 垃圾收集周期


当该标志被开启时,JVM 通过 CMSInitiatingOccupancyFraction 的值进行每一次 CMS 收集,而不仅仅是第一次。(否则后续会动态控制回收阈值)

(慎用) 因此,只有当我们充足的理由(比如测试)并且对应用程序产生的对象的生命周期有深刻的认知时,才应该使用该标志。


老年代的增长是由于部分对象在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 的!对于要求低延时的应用来说这是无法接受的!



[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 耗时会出现过程 gc;缩短的话,remark 阶段又会变长,而且是 STW,更不能接受。


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

第二次调优


增加 -XX:+CMSScavengeBeforeRemark 不是没有代价的,因为这会增加一次 Minor GC 停顿。所以这个方案好或者不好的判断标准就是:增加 CMSScavengeBeforeRemark 参数之后的 minor GC 停顿时间 + remark 停顿时间如果比增加之前的 remark GC 停顿时间要小,这才是好的方案。


-XX:+CMSScavengeBeforeRemark: 在 CMS GC 前启动一次 ygc,目的在于减少 old gen 对 ygc gen 的引用,降低 remark 时的开销-----一般 CMS 的 GC 耗时 80%都在 remark 阶段

第二次调整的结果


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



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 好得多了

总结

解决 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。

发布于: 2021 年 06 月 18 日阅读数: 165
用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论

发布
暂无评论
☕【JVM性能调优】「CMS垃圾回收器」调优化方案