【JVM性能优化】面向CMS垃圾回收器的性能优化方案

简介: 【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的收集阶段

image.png


  • 箭头1 显示abortable-preclean阶段耗时4.04秒
  • 箭头2 显示的是remark阶段,耗时0.11秒
  • 虽然abortable-preclean阶段是concurrent的,不会暂停其他的用户线程。就算不优化,可能影响也不大。


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

image.png


统计期间内(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的时间也就不会太长




第一次调整参数

image.png

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

image.png

[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)更低

image.png


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

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

image.png

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

image.png

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

image.png

所以,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。













相关文章
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
1037 55
|
Prometheus 监控 算法
CMS圣经:CMS垃圾回收器的原理、调优,多标+漏标+浮动垃圾 分析与 研究
本文介绍了CMS(Concurrent Mark-Sweep)垃圾回收器的工作原理、优缺点及常见问题,并通过具体案例分析了其优化策略。重点探讨了CMS的各个阶段,包括标记、并发清理和重标记
CMS圣经:CMS垃圾回收器的原理、调优,多标+漏标+浮动垃圾 分析与 研究
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
缓存 算法 Java
JVM深入原理(八)(一):垃圾回收
弱引用-作用:JVM中使用WeakReference对象来实现软引用,一般在ThreadLocal中,当进行垃圾回收时,被弱引用对象引用的对象就直接被回收.软引用-作用:JVM中使用SoftReference对象来实现软引用,一般在缓存中使用,当程序内存不足时,被引用的对象就会被回收.强引用-作用:可达性算法描述的根对象引用普通对象的引用,指的就是强引用,只要有这层关系存在,被引用的对象就会不被垃圾回收。引用计数法-缺点:如果两个对象循环引用,而又没有其他的对象来引用它们,这样就造成垃圾堆积。
310 0
|
算法 Java 对象存储
JVM深入原理(八)(二):垃圾回收
Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用哪一种GC算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为StopTheWorld简称STW,如果STW时间过长则会影响用户的使用。一般来说,堆内存越大,最大STW就越长,想减少最大STW,就会减少吞吐量,不同的GC算法适用于不同的场景。分代回收算法将整个堆中的区域划分为新生代和老年代。--超过新生代大小的大对象会直接晋升到老年代。
310 0
|
算法 网络协议 Java
【JVM】——GC垃圾回收机制(图解通俗易懂)
GC垃圾回收,标识出垃圾(计数机制、可达性分析)内存释放机制(标记清除、复制算法、标记整理、分代回收)
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
1044 6
|
8月前
|
存储 缓存 Java
我们来说一说 JVM 的内存模型
我是小假 期待与你的下一次相遇 ~
559 5
|
8月前
|
存储 缓存 算法
深入理解JVM《JVM内存区域详解 - 世界的基石》
Java代码从编译到执行需经javac编译为.class字节码,再由JVM加载运行。JVM内存分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)区域,其中堆是GC主战场,方法区在JDK 8+演变为使用本地内存的元空间,直接内存则用于提升NIO性能,但可能引发OOM。