写点什么

🏆「终」【JVM 性能调优】「CMS 垃圾回收器」优化实战分析(补充版)

发布于: 3 小时前
🏆「终」【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:+UseConcMarkSweepGC-XX:CMSInitiatingOccupancyFraction=n-XX:+UseCMSInitiatingOccupancyOnly
复制代码


  • -XX:CMSInitiatingOccupancyFraction=n:CMS 回收器机制触发回收垃圾的百分比比重 jdk8 的时候为 92%。

  • -XX:+UseCMSInitiatingOccupancyOnly:是否每次都采用固化参数进行分配触发 MajorGC

NewRatio 参数参考

  • 默认的 NewRatio 为 2,表示新生代和老年代比例是 1:2,即占堆的 1/3


但是实际设置了-Xmx 和-Xms 后,新生代的大小不符合预期

原因:runtime.arguments.cpp
else if (UseConcMarkSweepGC) {    set_cms_and_parnew_gc_flags();}const size_t preferred_max_new_size_unaligned =    MIN2(max_heap/(NewRatio+1), ScaleForWordSize(young_gen_per_worker * parallel_gc_threads));
复制代码

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

  1. cms 并发周期执行期间,用户的线程依然在运行,如果这时候如果应用线程向老年代请求分配的空间超过预留的空间,就会抛出该错误 - 后台线程的收集没有赶上应用线程的分配速度

  2. 有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多(内存碎片)导致暂时性的空间不足,而浮动垃圾就是 cms 执行期间用户线程申请的内存空间这个错误可能触发两种情况

  3. cms 的 foreground 模式(默认的 cms gc 属于 background 模式),这个模式是 CMS 自己的 mark-sweep 不实现做并发的(串行的)old generation GC,不过会将一些阶段省略掉

  4. 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 这两个参数


  1. 所以通常来说不建议设置上面两个参数,否则可能在 Java8 中会触发 foreground collector可能会更慢(单线程)所以通常当出现concurrent mode failure时触发的都是 Serial Old GC

  2. 关于 UseCMSCompactAtFullCollection 和 CMSFullGCsBeforeCompaction 的警告源代码

runtime\arguments.cpp
 if (FLAG_IS_CMDLINE(UseCMSCompactAtFullCollection)) {    warning("UseCMSCompactAtFullCollection is deprecated and will likely be removed in a future release.");  }  if (FLAG_IS_CMDLINE(CMSFullGCsBeforeCompaction)) {    warning("CMSFullGCsBeforeCompaction is deprecated and will likely be removed in a future release.");  }
复制代码


  1. 关于用哪种处理方式的源代码 gc_implementation/concurrentMarkSweep/concurrentMarkSweepGeneration.cpp


void CMSCollector::acquire_control_and_collect{...bool should_compact    = false;decide_foreground_collection_type(clear_all_soft_refs,    &should_compact, &should_start_over);...
if (should_compact) {...// 这个就是mark-sweep-compact 的 Full GCdo_compaction_work(clear_all_soft_refs);...
}else { // mark-sweep do_mark_sweep_work(clear_all_soft_refs, first_state, should_start_over);}
*should_compact = UseCMSCompactAtFullCollection && ((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction) || GCCause::is_user_requested_gc(gch->gc_cause()) || gch->incremental_collection_will_fail(true /* consult_young */));
复制代码


而 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

  1. 对于 Java8 来说,这个主要是在 metaspace 扩容时触发的

  2. 如果老年代设置了 CMS,则 Metasapce 扩容引起的 FGC 会转变成一次 CMS

  3. Java8 中收集器默认就会收集元空间中不再载入的类


在刚启动应用后,通过jstat -gccause pid后看到出现了 fgc,此时 ou 也没有占用


  • 通常这种情况是上面提到的 metaspace 扩容引起的,从 LGCC 也可以看到Metadata GC Threshold,触发的原因是因为 Metaspace 大小达到了 GC 阈值

  • MetaspaceSize 主要是控制 metaspaceGC 发生的初始阈值,也是最小阈值,但是触发 metaspaceGC 的阈值是不断变化的


 jstat -gccause 23270 1000  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC     GCC                   0.00  25.87  82.46   0.00  97.47  94.80      1    0.124     2    0.096    0.220 Metadata GC Threshold No GC
复制代码


通过观察 gc 日志,出现 cms 异常的几种情况


[ParNew (promotion failed): ... (concurrent mode failure):...
复制代码


这种情况是先出现了promotion failed,然后准备触发 fgc。


而此时 cms 这在执行并发收集,此时则执行打断逻辑,输出 concurrent mode failure


具体源代码也是concurrentMarkSweepGeneration.cpp


if (first_state > Idling) {    report_concurrent_mode_interruption();}[ParNew (promotion failed): ...
复制代码


这种情况就是单纯出现了 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 相关问题

基础参数:

-Xloggc:gc_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
复制代码

可选调试参数:

 -XX:+PrintGCApplicationStoppedTime # 打印停顿日志 -XX:+PrintTenuringDistribution:打印升迁日志 -XX:+PrintPromotionFailure # 打印分派失败 -XX:+PrintHeapAtGC gc 进行打印 -XX:PrintFLSStatistics=1  较少使用 主要用于统计计算
复制代码

cms 相关

  1. 物理机内存:16G

  2. 预估老年代常驻对象如 Player 3000,一个 Player 平均 2M,大约 6G,所以老年代比如建议 10G

  3. -Xms12G -Xmx12G

  4. 设置新生代 2G,老年代 10G

  5. 设置 CMSInitiatingOccupancyFraction 为 70,则老年代剩余空间为 3G,大于新生代大小

  6. 可选:-XX:+CMSScavengeBeforeRemark

简单算法:

-XX:NewRatio=4,即新生代和老年代 1:4


然后设置 CMSInitiatingOccupancyFraction 为 70,即老年代剩余空间稍大新生代但要保证这个 70 基本上要大于老年代常驻内存,否则可能会频繁 cms gc


另外建议增加脚本,尝试手动执行 fgc,整理碎片


如每天凌晨 3 点


jstat -gccause pid >> cms.logjmap -histo pid >> cms.log
jstat -gccause pid >> cms.logjmap -histo:live pid >> cms.log
复制代码

metaspace

设置 -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m,注意如果设置的过小,则会引起 fgc 甚至 metaspace oom

发布于: 3 小时前阅读数: 7
用户头像

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

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

评论

发布
暂无评论
🏆「终」【JVM 性能调优】「CMS 垃圾回收器」优化实战分析(补充版)