一个有意思的CMS问题

作者: 美团Java | 来源:发表于2017-10-12 20:28 被阅读3368次

    简书 占小狼 转载请注明原创出处,谢谢!

    大家新年好,愿你们在新的一年顺利晋升、工资涨涨涨...

    之前无意间碰到一个有趣的CMS GC问题,问题很简单,现象很粗暴。

    代码

    /**
     * -Xmx20m -Xms20m -Xmn10m -XX:+UseParNewGC  -XX:+UseConcMarkSweepGC
     * -XX:+UseCMSInitiatingOccupancyOnly  -XX:CMSInitiatingOccupancyFraction=75
     * -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC
     */
    public class JVM {
    
        private static final int _1MB = 1024 * 1024;
    
        public static void main(String[] args) throws Exception {
    
            byte[] b1 = new byte[2 * _1MB];
            byte[] b2 = new byte[2 * _1MB];
            byte[] b3 = new byte[2 * _1MB];
            byte[] b4 = new byte[4 * _1MB];
            System.in.read();
        }
    }
    

    现象

    程序运行之后,执行jstat -gcutils pid 1000命令,结果如下:

    在JVM参数中已经设置了-XX:+UseCMSInitiatingOccupancyOnly-XX:CMSInitiatingOccupancyFraction=75

    只有在老年代的使用率达到75%时才会触发CMS回收,可目前的现象是老年代使用率才60%,就开始不停的GC、不停的GC、不停的GC,GC日志如下:

    看这架势,应该是在不停的发生CMS GC了。

    原因查找

    既然一直在触发CMS,那问题根本因为在触发CMS的条件中,之前以为只要设置了-XX:+UseCMSInitiatingOccupancyOnly参数,只有在老年代的使用率达到阈值时才会触发。

    翻了代码之后才发现,问题没这么简单,触发CMS的判断逻辑位于CMSCollector::shouldConcurrentCollect()方法中,实现如下:

    在设置了-XX:+UseCMSInitiatingOccupancyOnly参数的前提下,有三种情况会触发:
    1、老年代当前使用率是否达到阈值CMSInitiatingOccupancyFraction;
    2、判断当前新生代的对象是否能够全部顺利的晋升到老年代,如果不能,就提早触发一次老年代的收集,这是本案例中不停CMS的根本原因,incremental_collection_will_fail(true)实现如下:

    其中get_gen(0)指向当前年轻代的堆,因为设置了-XX:+UseParNewGC,则年轻代的堆实现是ParNewGeneration,该类继承了DefNewGeneration,方法collection_attempt_is_safe()位于DefNewGeneration类中,实现如下:

    前面2个条件先忽略,看最后一个条件,_next_gen指向老年代的堆,其中promotion_attempt_is_safe()实现如下:

    传入的参数max_promotion_in_bytes,由年轻代的used方法计算得到,eden区的使用量 + from区的使用量

    size_t DefNewGeneration::used() const {
      return eden()->used()
           + from()->used();      // to() is only used during scavenge
    }
    

    其中promotion_attempt_is_safe()方法中的变量
    1、available是老年代的可用内存大小
    2、av_promo每次YGC时晋升到老年代对象大小的平均值

    当老年代的可用内存大于av_promo,或者大于max_promotion_in_bytes时,说明下次的YGC是安全的,否则返回fasle,提早进行一次CMS操作,释放老年代的空间,以容纳下次YGC晋升上来的对象。

    到这里,本文中的例子不断的进行CMS GC的疑惑应该可以解释清楚了。

    别忘了,还有第三种情况:

    if (CMSClassUnloadingEnabled && _permGen->should_concurrent_collect()) {
        bool res = update_should_unload_classes();
        if (res) {
          if (Verbose && PrintGCDetails) {
            gclog_or_tty->print_cr("CMS perm gen initiated");
          }
          return true;
        }
      }
    

    前提是设置了-XX:+CMSClassUnloadingEnabled,而且_permGen永久带的内存使用率达到了阈值CMSInitiatingPermOccupancyFraction,默认值是92。

    即使满足上面2个条件,还需要一层判断update_should_unload_classes()

    如果一开始永久代大小没有设置、或者设置的很小,很有可能一开始就执行CMS,这让很多同学表示怀疑,什么都没做,就给我来一次CMS的日志。

    相关文章

      网友评论

      • 两条鱼:在Java8 1.8.0_151 下复现不了, 执行该段代码只会有一次young gc.
      • 夏茂轩:大神表情会说话
      • 东江湖style:出现这种情况之后,距离OOM就不远了吧。老年代已经要放不下新生代里的对象了,如果等到新生代这些对象都晋升之后,老年代就放不下了,于是出现OOM。是这样的吗?
        美团Java:@东江湖style 1、shouldConcurrentCollect在周期性old GC会被调用,YGC不会判断这个;2、是不通过的函数入口,不过实现在同一个文件中。
        东江湖style:@占小狼 那请问CMSCollector::shouldConcurrentCollect()是什么情况下会得到调用呢?是在周期性old GC(默认2s的那个)时调用?还是在每次YGC之前?还是两种情况下都会被调用啊?
        还有一点我一直不太明白,主动old GC和周期性old GC是否是以同一个函数作为切入点(即调用同一个函数),然后在同一个函数里通过逻辑判断进入不同的分支从而执行不同的GC策略(压缩与否);还是完全是不同的GC程序入口?谢谢狼哥:pray:
        美团Java:@东江湖style 在OOM之前,JVM会尽可能回收可以回收的对象,如果实在空间不够,就发生OOM
      • VincentSky:为什么新生代跟老年代里面的数据加起来只有7MB左右?不是应该一共10MB的数据吗
        美团Java:@VincentSky 不会吧,你怎么看的?
        VincentSky:@占小狼 一共new了4个byte数组,加起来应该有10M大小。可是新生代跟老年代里面的数据加起来没有那么多
        美团Java:@VincentSky 嗯?
      • 白马王朗:你是在pc上测试的吗?
        美团Java:@白马王朗 嗯,大部分是的
        白马王朗:@占小狼 现在你们用cms?
        美团Java:@白马王朗 嗯
      • 2BetterM:为啥我复现不了,内存使用比也不对头,使用的是jdk1.8
        S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
        0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
        0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
        0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
        0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
        0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
        0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
        0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001
        0.00 49.98 52.93 0.00 63.92 66.85 1 0.001 0 0.000 0.001

        Server compiler detected.
        JVM version is 25.111-b14
        Non-default VM flags: -XX:CICompilerCount=4 -XX:CMSInitiatingOccupancyFraction=75 -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=6 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=10485760 -XX:OldPLABSize=16 -XX:OldSize=10485760 -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseFastUnorderedTimeStamps -XX:+UseParNewGC
        Command line: -Xmx20m -Xms20m -Xmn10m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Didea.launcher.port=7534 -Didea.launcher.bin.path=/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
        美团Java:@2BetterM 我本地是用的1.7,换个试试看
      • pistolgao:阅读源码用的什么ide?CLion?
        美团Java:@pistolgao sublime
      • tuser:如果设置了参数-XX:+CMSClassUnloadingEnabled,且只要满足以下3个条件中的一个即可。看代码,第一个条件没看懂。占老板能给解释下么?
        tuser: @占小狼 恩,听着好有道理,哈哈哈
        美团Java:@tuser 猜测是每次卸载类会记一个时间戳,两次类卸载时间不能太近,这个没细看
      • 独角没有戏:当老年代的可用内存大于av_promo,或者大于max_promotion_in_bytes时,说明下次的YGC是安全的,否则返回fasle,提早进行一次CMS操作,释放老年代的空间,以容纳下次YGC晋升上来的对象。

        根据文中资料,对比数据,b4 进入新生代时 b1 b2 b3 已经在老年代 。b4 在新生代占4M ,40%,其余三个在 老年代共占 60%。此时CMS GC不是内存分配引起,而是根据“计算结果”判断是否进行“提早CMS收集”....终于懂了,感谢大佬
        美团Java:@独角没有戏 哈哈,不客气
      • 壹拾贰:没开启过 不知道会不会看不到有效的关键gc信息 像我这种没深入了解过 也没源码能力的 大致就拿着gc cause的log信息 goggle了:joy:
        壹拾贰:@占小狼 向占老板看齐:smile:
        美团Java:@壹拾贰 哈哈,多看看就熟练了
      • 壹拾贰:占老板神速:smile:看样子下次遇到gc问题 对于我这种没源码阅读能力的可以开启verbose参数啊
        美团Java:@壹拾贰 -verbose:gc
        壹拾贰:@占小狼 今天检查发现启动参数并有没有Verbose这个option,看来遇到问题还是源码给力:sweat:
        美团Java:开启verbose参数也没软用啊,看到一堆的日志,无从下手

      本文标题:一个有意思的CMS问题

      本文链接:https://www.haomeiwen.com/subject/mwboyxtx.html