🏆「终」【JVM 性能调优】「CMS 垃圾回收器」优化实战分析(补充版)
学习背景
关于 CMS GC 介绍和调优的文章比较多,但大多没有经过验证。因为 CMS 目前在 Java9 之前还是相对用的较多(G1 也需要持续去调研),所以这里把 CMS 的一些重要知识和调优经验总结一下。
相关 jvm 源代码版本为/openjdk-8-src-b132-03_mar_2014/openjdk/hotspot/src/share/vm,个人建议还是选择 openjdk7 比较好,因为是行业标准!
除了 OpenJDK 的源代码和 R 大以外,什么都不要轻易相信。
CMS 的一些重要知识点
使用 CMS GC 必备的三个参数
-XX:CMSInitiatingOccupancyFraction=n:CMS 回收器机制触发回收垃圾的百分比比重 jdk8 的时候为 92%。
-XX:+UseCMSInitiatingOccupancyOnly:是否每次都采用固化参数进行分配触发 MajorGC
NewRatio 参数参考
默认的 NewRatio 为 2,表示新生代和老年代比例是 1:2,即占堆的 1/3
但是实际设置了-Xmx 和-Xms 后,新生代的大小不符合预期
原因:runtime.arguments.cpp
cms 新生代的大小是计算出来的
所以通常使用 cms 的时候,建议手动指定新生代大小参数
(-XX:NewRatio 或者-Xmn 或者-XX:NewSize/-XX:MaxNewSize)
另外 JDK-6862534 : -XX:NewRatio completely ignored when combined with -XX:+UseConcMarkSweepGC,之前是即使手动指定 -XX:NewRatio,也无效,现早已修复
使用
jstat -gccause pid
观察 cms fgc 的时候,发现每次到阈值回收的时候,fgc 每次会跳 2 次
因为 cms 的一个并发周期内有两个阶段 initial mark 与 final re-mark,这两个阶段都是"stop the world"‘,不过暂停时间较短
而 jstat 的这个 fgc 的计数器是说的应用暂停的次数,注意这里所指的是'cms gc'引起的 stw,详细可参考 jstat 显示的 full GC 次数与 CMS 周期的关系
如果观察 cms fgc,突然发现 stw 的时间很长,多达几秒甚至更多,一定是出现了异常情况,而这些情况的代价都十分昂贵,在做 cms 调优的时候要尽可能的避免
concurrent mode failure
cms 并发周期执行期间,用户的线程依然在运行,如果这时候如果应用线程向老年代请求分配的空间超过预留的空间,就会抛出该错误 - 后台线程的收集没有赶上应用线程的分配速度
有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多(内存碎片)导致暂时性的空间不足,而浮动垃圾就是 cms 执行期间用户线程申请的内存空间,这个错误可能触发两种情况
cms 的 foreground 模式(默认的 cms gc 属于 background 模式),这个模式是 CMS 自己的 mark-sweep 不实现做并发的(串行的)old generation GC,不过会将一些阶段省略掉。
CMS 的 foreground collector 的算法就是普通的 mark-sweep。它收集的范围只是 CMS 的 old generation,而不包括其它 generation(有争议哦,标记会涉及到 young 区以及同时也可以 scavenge 机制预先触发 minorgc)。因而它在 HotSpot VM 里不叫做 full GC
Serial Old GC
mark-sweep-compact 算法
它收集的范围是整个 GC 堆,包括 Java heap 的 young generation 和 old generation,以及 non-Java heap 的 permanent generation。因而其名 Full GC
前者的出现原因
A STW foreground collection can pick up where a concurrent background collection left off to try to avoid a full GC. This is nice but normally it has worse performance than a full GC。
即是为了避免 fgc,但是往往性能甚至比 fgc 更差。
对于第一种 foreground 模式,必须要 -XX:-UseCMSCompactAtFullCollection 和 -XX:CMSFullGCsBeforeCompaction 设置大于 0
但是 UseCMSCompactAtFullCollection 默认为 true,CMSFullGCsBeforeCompaction 默认是 0(每次 fullgc 都会进行压缩!),所以一定会触发第二种 Serial Old GC
参考:
https://bugs.openjdk.java.net/browse/JDK-8010202
https://bugs.openjdk.java.net/browse/JDK-8064702
https://bugs.openjdk.java.net/browse/JDK-8027132
均建议 foreground collector 在 Java8 废弃,在 Java9 移除,包括 UseCMSCompactAtFullCollection 和 CMSFullGCsBeforeCompaction 这两个参数
所以通常来说不建议设置上面两个参数,否则可能在 Java8 中会触发 foreground collector,可能会更慢(单线程)。所以通常当出现
concurrent mode failure
时触发的都是 Serial Old GC关于 UseCMSCompactAtFullCollection 和 CMSFullGCsBeforeCompaction 的警告源代码
runtime\arguments.cpp
关于用哪种处理方式的源代码 gc_implementation/concurrentMarkSweep/concurrentMarkSweepGeneration.cpp
而 should_compact 主要的一个判断逻辑就是判断 UseCMSCompactAtFullCollection 和 CMSFullGCsBeforeCompaction 这两个参数
concurrent promotion failed
Java Performance,The Definitive Guide 的原文是这样描述的:
原文:
Here, CMS started a young collection and assumed that there was enough free space to hold all the promoted objects (otherwise, it would have declared a concurrent mode failure). That assumption proved incorrect: CMS couldn’t promote the objects because the old generation was fragmented (or, much less likely, because the amount of memory to be promoted was bigger than CMS expected).
翻译:
新生代垃圾收集,判断老年代似乎有足够的空闲空间可以容纳所有的晋升对象(否则,CMS 收集器会报 concurrent mode failure)。这个假设最终被证明是错误的,由于老年代空间的碎片化(或者,不太贴切的说,由于晋升实际要占用的内存超过了 CMS 收集器的判断),CMS 收集器无法晋升这些对象。
原文:
Sometimes we see these promotion failures even when thelogs show that there is enough free space in tenured generation. The reason is'fragmentation' - the free space available in tenured generation is notcontiguous, and promotions from young generation require a contiguous freeblock to be available in tenured generation. CMS collector is a non-compactingcollector, so can cause fragmentation of space for some type of applications.
翻译:
CMS 收集器对老年代收集的时候,不再进行任何压缩和整理的工作,意味着老年代随着应用的运行会变得碎片化;碎片过多会影响大对象的分配,虽然老年代还有很大的剩余空间,但是没有连续的空间来分配大对象
如果在 ParNew 准备收集时 CMS 说晋升没问题,但 ParNew 已经开始收集之后确实遇到了晋升失败的情况。
promotion failed 是说,担保机制确定老年代是否有足够的空间容纳新来的对象,如果担保机制说有,但是真正分配的时候发现由于碎片导致找不到连续的空间而失败;而 concurrent mode failure 是指并发周期还没执行完,用户线程就来请求比预留空间更大的空间了,即后台线程的收集没有赶上应用线程的分配速度。
promotion failed 触发 fgc,触发模式同上,通常也是 Serial Old GC
permgen (or the metaspace) fills up
对于 Java8 来说,这个主要是在 metaspace 扩容时触发的
如果老年代设置了 CMS,则 Metasapce 扩容引起的 FGC 会转变成一次 CMS
Java8 中收集器默认就会收集元空间中不再载入的类
在刚启动应用后,通过jstat -gccause pid
后看到出现了 fgc,此时 ou 也没有占用
通常这种情况是上面提到的 metaspace 扩容引起的,从 LGCC 也可以看到
Metadata GC Threshold
,触发的原因是因为 Metaspace 大小达到了 GC 阈值MetaspaceSize 主要是控制 metaspaceGC 发生的初始阈值,也是最小阈值,但是触发 metaspaceGC 的阈值是不断变化的
通过观察 gc 日志,出现 cms 异常的几种情况
这种情况是先出现了promotion failed
,然后准备触发 fgc。
而此时 cms 这在执行并发收集,此时则执行打断逻辑,输出 concurrent mode failure
具体源代码也是concurrentMarkSweepGeneration.cpp
这种情况就是单纯出现了 promotion failed,此时 cms 未执行并发收集(concurrent mode failure): ...
这种情况是单纯的 cms 正在执行并发收集,然后用户线程申请内存空间不足 jvm 有一个内存担保机制,是类似于判断'老年代最大的可用连续空间是否大于新生代所有对象的总和'。但通常描述 promotion failed 的时候是指担保机制够了, 才会发生。那么既然有最大可用连续空间,为什么还会 failed with 5.0 because a single contiguous chunk of space is not requiredfor promotions,即在 jdk5 后,晋升不需要连续空间了所以这里的担保是指'老年代是否有足够的空间容纳要晋升的对象',而不是连续空间。那么出现 fail,则是碎片问题
CMS 优化方向
原则
cms 的的优势就是低延迟,但是如果出现了长时间的 stw,则对应用程序有很大的影响如果出现了 concurrent mode failure 和 promotion failed,代价都非常昂贵,我们调优应该尽量避免这些情况
针对 concurrent mode failure 的优化
发生该失败的主要原因是由于 CMS 不能以足够快的速度清理老年代空间
当老年代空间的占用达到某个阈值时,并发回收就开始了。一个 CMS 后台线程开始扫描老年代空间,寻找无用的垃圾对象时,竞争就开始了。CMS 收集器必须在老年代剩余的空间用尽之前,完成老年代空间的扫描及回收工作。否则如果在正常速度的比赛中失效,就会发生该错误
在并发清理阶段,用户线程仍然在运行,必须预留出空间给用户线程使用,会产生’浮动垃圾‘
常规优化途径如下
以更高的频率执行后台的回收线程,即提高 CMS 并发周期发生的频率
主要是调低 CMSInitiatingOccupancyFraction 的值
但是不能太低,太低会导致过于频繁的 gc,会消耗更多的的 cpu 和停顿
需要先计算老年代常驻内存大小,如占用 60%,那么这个阈值则可以设置为约 70%,否则会比较频繁 gc
可以考虑担保机制,只要老年代预留剩余空间大于年轻代大小,比如新生代和老年代的比例是 1 : 4,即新生代占用老年代的 25%,那么这个阈值可以设置为 70,即老年代还预留出来 30%的空间
注意如果浮动垃圾很多的话,也无法解决该问题,即 cms 并发回收期间,浮动垃圾越来越多,占用预留空间,多次的 ygc 的话,会有填满预留空间的可能,虽然概率较低
两个条件综合考虑,如果设置了阈值 70,但是老年代常驻内存很大,甚至超过 70,那么此时的建议要提高堆内存,增加老年代的大小或者减少新生代的大小
针对 promotion failed 的优化
这个是 cms 最为严重的’碎片问题‘,我们要尽量避免这个发生后引起的 fgc
所以优化这个问题,也可以描述为'如何解决碎片问题'
常规优化途径如下
增大堆内存,增加老年代大小,但要注意不要超过 32g(the HotSpot JVM uses a trick to compress object pointers when heaps are less than around 32 GB)
尽早执行 cms gc,合理设置 CMSInitiatingOccupancyFraction,会合并老生代中相邻的 free 空间,可分配给较大的对象
和上面一样,也可以做一个老年代预留空间大于年轻代
到了阈值后,就会触发 cms gc,但还是和上面说的,会产生浮动垃圾 + 碎片,还是会出现
另外一个比较“挫”的办法,是在每天凌晨访问量低的时候,主动执行一下 fgc,执行一下'碎片压缩'
如 System.gc,但是要注意是否开启了 -XX:+ExplicitGCInvokesConcurrent,-XX:DisableExplictGC
所以建议办法是用jmap -histo:live
另外晋升还包括 to space 空间小,可以根据情况尝试提高 Survivor
CMS 实战参数
日志,主要是用来排查 cms 相关问题
基础参数:
可选调试参数:
cms 相关
物理机内存:16G
预估老年代常驻对象如 Player 3000,一个 Player 平均 2M,大约 6G,所以老年代比如建议 10G
-Xms12G -Xmx12G
设置新生代 2G,老年代 10G
设置 CMSInitiatingOccupancyFraction 为 70,则老年代剩余空间为 3G,大于新生代大小
可选:-XX:+CMSScavengeBeforeRemark
简单算法:
-XX:NewRatio=4,即新生代和老年代 1:4
然后设置 CMSInitiatingOccupancyFraction 为 70,即老年代剩余空间稍大新生代但要保证这个 70 基本上要大于老年代常驻内存,否则可能会频繁 cms gc
另外建议增加脚本,尝试手动执行 fgc,整理碎片
如每天凌晨 3 点
metaspace
设置 -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m,注意如果设置的过小,则会引起 fgc 甚至 metaspace oom
版权声明: 本文为 InfoQ 作者【李浩宇/Alex】的原创文章。
原文链接:【http://xie.infoq.cn/article/1248fe8be86d975346dd9dc58】。文章转载请联系作者。
评论