深入理解CMS GC

深入理解CMS GC

背景

  1. 网上关于cms gc介绍和调优的文章比较多,但大多没有经过验证。因为cms目前在Java9之前还是相对用的较多(G1也需要持续去调研),所以这里把CMS的一些重要知识和调优经验总结一下

  2. 相关jvm源代码版本为/openjdk-8-src-b132-03_mar_2014/openjdk/hotspot/src/share/vm

    除了OpenJDK的源代码和R大以外,什么都不要轻易相信

CMS的一些重要知识点

  1. 使用cms gc必备的三个参数

    -XX:+UseConcMarkSweepGC
    -XX:CMSInitiatingOccupancyFraction=n
    -XX:+UseCMSInitiatingOccupancyOnly
    
  2. 默认的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,也无效,现早已修复

  3. 使用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周期的关系
  4. 如果观察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,不过会将一些阶段省略掉。
         + CMS的foreground collector的算法就是普通的mark-sweep。它收集的范围只是CMS的old generation,而不包括其它generation。因而它在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,所以一定会触发第二种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这两个参数
    4. 所以通常来说不建议设置上面两个参数,否则可能在Java8中会触发foreground collector,可能会更慢(单线程)。所以通常当出现concurrent mode failure时触发的都是Serial Old GC
    
    1. 关于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.");
      }
    
    2. 关于用哪种处理方式的源代码 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 GC
    do_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这两个参数
    
    • promotion failed
    1. 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收集器无法晋升这些对象。
    2. 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收集器对老年代收集的时候,不再进行任何压缩和整理的工作,意味着老年代随着应用的运行会变得碎片化;碎片过多会影响大对象的分配,虽然老年代还有很大的剩余空间,但是没有连续的空间来分配大对象
    3. 如果在ParNew准备收集时CMS说晋升没问题,但ParNew已经开始收集之后确实遇到了晋升失败的情况
    4. promotion failed是说,担保机制确定老年代是否有足够的空间容纳新来的对象,如果担保机制说有,但是真正分配的时候发现由于碎片导致找不到连续的空间而失败;而concurrent mode failure是指并发周期还没执行完,用户线程就来请求比预留空间更大的空间了,即后台线程的收集没有赶上应用线程的分配速度。
    5. promotion failed触发fgc,触发模式同上,通常也是Serial Old GC
    
    • permgen (or the metaspace) fills up
    1. 对于Java8来说,这个主要是在metaspace扩容时触发的
    2. 如果老年代设置了 CMS,则 Metasapce 扩容引起的 FGC 会转变成一次 CMS
    3. Java8中收集器默认就会收集元空间中不再载入的类
    
  5. 在刚启动应用后,通过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
    
  6. 通过观察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正在执行并发收集,然后用户线程申请内存空间不足
  7. jvm有一个内存担保机制,是类似于判断'老年代最大的可用连续空间是否大于新生代所有对象的总和'。但通常描述promotion failed的时候是指担保机制够了, 才会发生。那么既然有最大可用连续空间,为什么还会failed

    • with 5.0 because a single contiguous chunk of space is not required
      for promotions,即在jdk5后,晋升不需要连续空间了
    • 所以这里的担保是指'老年代是否有足够的空间容纳要晋升的对象',而不是连续空间。那么出现fail,则是碎片问题

CMS优化方向

  1. 原则

    • cms的的优势就是低延迟,但是如果出现了长时间的stw,则对应用程序有很大的影响
    • 如果出现了concurrent mode failure和promotion failed,代价都非常昂贵,我们调优应该尽量避免这些情况
  2. 针对concurrent mode failure的优化

    • 发生该失败的主要原因是由于CMS不能以足够快的速度清理老年代空间

    • 当老年代空间的占用达到某个阈值时,并发回收就开始了。一个CMS后台线程开始扫描老年代空间,寻找无用的垃圾对象时,竞争就开始了。CMS收集器必须在老年代剩余的空间用尽之前,完成老年代空间的扫描及回收工作。否则如果在正常速度的比赛中失效,就会发生该错误

    • 在并发清理阶段,用户线程仍然在运行,必须预留出空间给用户线程使用,会产生’浮动垃圾‘

    • 常规优化途径如下:

      以更高的频率执行后台的回收线程,即提高CMS并发周期发生的频率

      • 主要是调低CMSInitiatingOccupancyFraction的值

      • 但是不能太低,太低会导致过于频繁的gc,会消耗更多的的cpu和停顿

      • landon

        需要先计算老年代常驻内存大小,如占用60%,那么这个阈值则可以设置为约70%,否则会比较频繁gc

        可以考虑担保机制,只要老年代预留剩余空间大于年轻代大小,比如新生代和老年代的比例是1 : 4,即新生代占用老年代的25%,那么这个阈值可以设置为70,即老年代还预留出来30%的空间

        注意如果浮动垃圾很多的话,也无法解决该问题,即cms并发回收期间,浮动垃圾越来越多,占用预留空间,多次的ygc的话,会有填满预留空间的可能,虽然概率较低

        两个条件综合考虑,如果设置了阈值70,但是老年代常驻内存很大,甚至超过70,那么此时的建议要提高堆内存,增加老年代的大小或者减少新生代的大小

  3. 针对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

      • 所以建议办法是用jmap -histo:live

    • 另外晋升还包括to space空间小,可以根据情况尝试提高Survivor

CMS实战参数

  1. 日志,主要是用来排查cms相关问题

    基础参数:
    -Xloggc:gc_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
    
    可选调试参数:
     -XX:+PrintGCApplicationStoppedTime
     -XX:+PrintTenuringDistribution
     -XX:+PrintPromotionFailure
     -XX:+PrintHeapAtGC
     -XX:PrintFLSStatistics=1
    
  2. 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.log
    jmap -histo pid >> cms.log
    jstat -gccause pid >> cms.log
    jmap -histo:live pid >> cms.log
    
  3. metaspace

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

其他

  1. cms如果出现ygc时间较长,可以考虑可能是老年代碎片过多,解决方案也是尝试在业务低峰主动触发fgc执行压缩
  2. TODO 了解cms的free list
  3. TODO 学习Optimizing Java

参考

  1. R大
  2. 涤生的博客
  3. 其他
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • System.gc整理 System.gc()源码public static void gc() { Runtim...
    andersonoy阅读 2,924评论 0 1
  • # 前言 在 深入浅出 JVM GC(2) 中,我们介绍了一些 GC 算法,GC 名词,同时也留下了一个问题,就...
    莫那一鲁道阅读 1,111评论 1 4
  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,523评论 3 83
  • 作者:一字马胡 转载标志 【2017-11-12】 更新日志 日期更新内容备注 2017-11-12新建文章初版 ...
    beneke阅读 2,184评论 0 7
  • 声明:原创文章,转载请注明出处。http://www.jianshu.com/u/e02df63eaa87 1、J...
    唐影若凡阅读 1,134评论 0 6