美文网首页
CMS vs G1基本原理总结

CMS vs G1基本原理总结

作者: zh_harry | 来源:发表于2020-07-21 15:00 被阅读0次

    CMS

    CMS(Concurrent Mark Sweep)收集器是一种以获取​最短回收停顿时间​为目标的收集器。这是因为CMS收集器工作时,GC工作线程与用户线程可以​并发(Concurrent)​执行,以此来达到降低收集停顿时间的目的

    CMS GC步骤

    初始标记(STW initial mark)

    1. 标记GC-Root 直接引用的老年代对象

    2. 年轻代Live对象直接引用的老年代对象(CMS 初始标记需要遍历年轻代,gc root除了一般定义的那些节点外,还需要加上从年轻代到老年代的引用)

    https://www.zhihu.com/question/53613423/answer/135743258 一组活跃对象R大解释

    分代式GC是一种部分收集(partial collection)的做法。在执行部分收集时,从GC堆的非收集部分指向收集部分的引用,也必须作为GC roots的一部分。

    GC ROOTS=normal gc roots+(非收集指向收集部分的引用)官方解释 https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html

    ## Pauses
    The CMS collector pauses an application twice during a concurrent 
    collection cycle. The first pause is to mark as live the objects directly 
    reachable from the roots (for example, object references from application 
    thread stacks and registers, static objects and so on) and from elsewhere 
    in the heap (for example, the young generation). This first pause is 
    referred to as the *initial mark pause*. The second pause comes at the 
    end of the concurrent tracing phase and finds objects that were missed 
    by the concurrent tracing due to updates by the application threads of 
    references in an object after the CMS collector had finished tracing that 
    object. This second pause is referred to as the *remark pause*.
    
    image

    参见深入理解JVM 第三版 CMS并未记录年轻代到老年代的代际引用

    image

    并发标记(Concurrent marking)

    并发标记阶段的主要工作是, 通过遍历第一个阶段(Initial Mark)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象 。(Current obj 对象在并发标记的同时被应用程序修改了引用关系,所以这里会被标记为dirty card)

    不过card table还有其他作用

    进行Minor GC时,如果有老年代引用新生代,怎么识别?

    当有老年代引用新生代,对应的card table被标识为相应的值(card table中是一个byte,有八位,约定好每一位的含义就可区分哪个是引用新生代,哪个是并发标记阶段修改过的)。P172 The Garbage Collection Handbook

    image

    并发预清理(Concurrent precleaning)

    在并发预清洗阶段, 将会重新扫描前一个阶段标记的Dirty对象,并标记被Dirty对象直接或间接引用的对象,然后清除Card标识 。

    image image

    可终止的预处理

    该阶段不是一定会发生,但很重要,相对也比较复杂!该阶段发生的前提是,新生代Eden区的内存使用量大于参数​CMSScheduleRemarkEdenSizeThreshold​ 默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。

    为什么需要这个阶段,存在的价值是什么?

    目标:因为CMS GC的终极目标是降低垃圾回收时的暂停时间(STW),所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。

    在该阶段,主要循环的做两件事:

    1)处理 From 和 To 区的对象,标记可达的老年代对象

    2)和上一个阶段一样,扫描处理Dirty Card中的对象

    当然了,这个逻辑不会一直循环下去,打断这个循环的条件有两个:

    // Try and schedule the remark such that young gen
    // occupancy is CMSScheduleRemarkEdenPenetration %.
    void CMSCollector::abortable_preclean() {
      check_correct_thread_executing();
      assert(CMSPrecleaningEnabled,  "Inconsistent control state");
      assert(_collectorState == AbortablePreclean, "Inconsistent control state");
    
      // If Eden's current occupancy is below this threshold,
      // immediately schedule the remark; else preclean
      // past the next scavenge in an effort to
      // schedule the pause as described avove. By choosing
      // CMSScheduleRemarkEdenSizeThreshold >= max eden size
      // we will never do an actual abortable preclean cycle.
      if (get_eden_used() > CMSScheduleRemarkEdenSizeThreshold) {
        TraceCPUTime tcpu(PrintGCDetails, true, gclog_or_tty);
        CMSPhaseAccounting pa(this, "abortable-preclean", _gc_tracer_cm->gc_id(), !PrintGCDetails);
        // We need more smarts in the abortable preclean
        // loop below to deal with cases where allocation
        // in young gen is very very slow, and our precleaning
        // is running a losing race against a horde of
        // mutators intent on flooding us with CMS updates
        // (dirty cards).
        // One, admittedly dumb, strategy is to give up
        // after a certain number of abortable precleaning loops
        // or after a certain maximum time. We want to make
        // this smarter in the next iteration.
        // XXX FIX ME!!! YSR
        size_t loops = 0, workdone = 0, cumworkdone = 0, waited = 0;
        while (!(should_abort_preclean() ||
                 ConcurrentMarkSweepThread::should_terminate())) {
          workdone = preclean_work(CMSPrecleanRefLists2, CMSPrecleanSurvivors2);
          cumworkdone += workdone;
          loops++;
          // Voluntarily terminate abortable preclean phase if we have
          // been at it for too long.
          if ((CMSMaxAbortablePrecleanLoops != 0) &&
              loops >= CMSMaxAbortablePrecleanLoops) {
            if (PrintGCDetails) {
              gclog_or_tty->print(" CMS: abort preclean due to loops ");
            }
            break;
          }
          if (pa.wallclock_millis() > CMSMaxAbortablePrecleanTime) {
            if (PrintGCDetails) {
              gclog_or_tty->print(" CMS: abort preclean due to time ");
            }
            break;
          }
          // If we are doing little work each iteration, we should
          // take a short break.
          if (workdone < CMSAbortablePrecleanMinWorkPerIteration) {
            // Sleep for some time, waiting for work to accumulate
            stopTimer();
            cmsThread()->wait_on_cms_lock(CMSAbortablePrecleanWaitMillis);
            startTimer();
            waited++;
          }
        }
        if (PrintCMSStatistics > 0) {
          gclog_or_tty->print(" [%d iterations, %d waits, %d cards)] ",
                              loops, waited, cumworkdone);
        }
      }
      CMSTokenSync x(true); // is cms thread
      if (_collectorState != Idling) {
        assert(_collectorState == AbortablePreclean,
               "Spontaneous state transition?");
        _collectorState = FinalMarking;
      } // Else, a foreground collection completed this CMS cycle.
      return;
    }
    
    1. 可以设置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是0,意思没有循环次数的限制。

    2. 如果执行这个逻辑的时间达到了阈值CMSMaxAbortablePrecleanTime,默认是5s,会退出循环(等待一次YGC)。

    2020-07-02T09:37:53.753+0800: 238204.309: [CMS-concurrent-abortable-preclean-start]
     CMS: abort preclean due to time 2020-07-02T09:37:58.785+0800: 238209.342: [CMS-concurrent-abortable-preclean: 3.128/5.032 secs] [Times: user=4.55 sys=0.91, real=5.03 secs] 
    

    参考:https://www.cnblogs.com/zhangxiaoguang/p/5792468.html

    3.128/5.032 secs 展示该阶段持续的时间和时钟时间(It is interesting to note that the user time reported is a lot smaller than clock time. Usually we have seen that real time is less than user time, meaning that some work was done in parallel and so elapsed clock time is less than used CPU time. Here we have a little amount of work – for 0.167 seconds of CPU time, and garbage collector threads were doing a lot of waiting. Essentially, they were trying to stave off for as long as possible before having to do an STW pause. By default, this phase may last for up to 5 seconds)

    1. 如果新生代Eden区的内存使用率达到了阈值CMSScheduleRemarkEdenPenetration,默认50%,会退出循环。(这个条件能够成立的前提是,在进行Precleaning时,Eden区的使用率小于10%
    if ((_collectorState == AbortablePreclean) && !_abort_preclean) {
      size_t used = get_eden_used();
      size_t capacity = get_eden_capacity();
      assert(used <= capacity, "Unexpected state of Eden");
      if (used >  (capacity/100 * CMSScheduleRemarkEdenPenetration)) {
        _abort_preclean = true;
      }
    }
    

    问题:如果超过50%退出,则永远不可能产品YGC,如果不退出,要等5秒浪费时间?

    1. 新生代小
    2. 50%避免REMARK 阶段产生YGC

    如果在AbortablePreclean阶段没来得及执行一次YGC,怎么办?

    • CMS算法中提供了一个参数:CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。
      不过,这种参数有利有弊,利是降低了Remark阶段的停顿时间,弊的是在新生代对象很少的情况下也多了一次YGC,最可怜的是在AbortablePreclean阶段已经发生了一次YGC,然后在该阶段又傻傻的触发一次。所以利弊需要把握。一般CMS的GC耗时 80%都在remark阶段,如果发现remark阶段停顿时间很长,可以尝试添加该参数

    重新标记(STW remark)

    预清理阶段也是并发执行的,并不一定是所有存活对象都会被标记,因为在并发标记的过程中对象及其引用关系还在不断变化中。

    因此, 需要有一个stop-the-world的阶段来完成最后的标记工作 ,这就是重新标记阶段(CMS标记阶段的最后一个阶段)。 主要目的是重新扫描之前并发处理阶段的所有残留更新对象 。

    image

    可以开启并行收集:-XX:+CMSParallelRemarkEnabled

    重新标记的内存范围是整个堆,包含younggen和oldgen 如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做cms的“gc root”,来扫描老年代

    主要工作:

    • 遍历新生代对象,重新标记;(新生代会被分块,多线程扫描)

    • 根据GC Roots,重新标记;

    • 遍历老年代的Dirty Card,重新标记。这里的Dirty Card,大部分已经在Preclean阶段被处理过了。

    [GC (CMS Final Remark) [YG occupancy: 1120265 K (2146944 K)]2019-08-30T17:59:30.896+0800: 9938.409:
    [Rescan (parallel) , 0.0345201 secs]2019-08-30T17:59:30.930+0800: 9938.444:
    [weak refs processing, 1.3559616 secs]2019-08-30T17:59:32.286+0800: 9939.800:
    [class unloading, 0.0689365 secs]2019-08-30T17:59:32.355+0800: 9939.869: [scrub symbol table, 0.0166362 secs]2019-08-30T17:59:32.372+0800: 9939.885:
    [scrub string table, 0.0013061 secs][1 CMS-remark: 21874968K(29071808K)] 22995234K(31218752K), 1.9651057 secs] [Times: user=2.80 sys=0.07, real=1.96 secs]
    

    并发清理(Concurrent sweeping)

    并发清理阶段,主要工作是 清理所有未被标记的死亡对象,回收被占用的空间 。

    image

    并发重置(Concurrent reset)

    并发重置阶段,将清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构 ,为下一个垃圾收集周期做好准备。

    image

    CMS缺点

    • CMS回收器采用的基础算法是Mark-Sweep。所有CMS不会整理、压缩堆空间。这样就会有一个问题:经过CMS收集的堆会产生空间碎片。 跟碎片相关的几个参数:

      1. -XX:+UseCMSCompactAtFullCollection:FULL GC后进行内存压缩,该参数与-XX:CMSFullGCsBeforeCompaction配合使用,该参数默认为true。-XX:CMSFullGCsBeforeCompaction:FULL GC多少次后进行内存压缩,CMS回退到full GC时用的算法是mark-sweep-compact(标记-清除-压缩 也叫 标记-整理),但compaction是可选的,不做的话碎片化会严重些但这次full GC的暂停时间会短些,这是个取舍。`

      2. -XX:+CMSFullGCsBeforeCompaction 说的是,在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的full GC才做一次压缩(而不是每10次CMS并发GC就做一次压缩,目前JVM里没有这样的参数)。这会使full GC更少做压缩,也就更容易使CMS的old gen受碎片化问题的困扰。 本来这个参数就是用来配置降低full GC压缩的频率,以期减少某些full GC的暂停时间。CMS回退到full GC时用的算法是mark-sweep-compact,但compaction是可选的,不做的话碎片化会严重些但这次full GC的暂停时间会短些;这是个取舍。`

    尽量不要出现 CMS的FULL GC,如果出现FULL GC频繁,优化不上去的话,可以考虑G1

    • 需要更多的CPU资源。从上面的图可以看到,为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU,单纯靠线程切 换是不靠谱的。并且,重新标记阶段,为空保证STW快速完成,也要用到更多的甚至所有的CPU资源。时间与吞吐需要平衡

    • CMS的另一个缺点是它需要更大的堆空间。因为CMS标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在CMS回收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。也就是说,CMS不会在老年代满的时候才开始收集。相反,它会尝试更早的开始收集。 – ​XX:CMSInitiatingOccupancyFraction =n 来设置这个阀值。​

    void ConcurrentMarkSweepGeneration::init_initiating_occupancy(intx io, uintx tr) {
    
     assert(io <= 100 && tr <= 100, "Check the arguments");
    
     if (io >= 0) {
    
       _initiating_occupancy = (double)io / 100.0;
    
    } else {
    
        _initiating_occupancy = ((100 - MinHeapFreeRatio) +
    
    (double)(tr * MinHeapFreeRatio) / 100.0)/ 100.0;
    
      }
    
    }
    

    其中两个参数

    1. io就是CMSInitiatingOccupancyFraction的值,如果你没设置过就是虚拟机自己的默认值,默认-1,可以用指令来查看:
    java -XX:+PrintFlagsFinal -version |grep CMSInitiatingOccupancyFraction
    
         intx CMSInitiatingOccupancyFraction            = -1 {product}
    
    java version "1.8.0_221"
    
    Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
    
    Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
    
    1. tr就是JVM启动参数CMSTriggerRatio的值,没手动设置过的话也同样也可以用指令来查看默认值:
     java -XX:+PrintFlagsFinal -version |grep CMSTriggerRatio
    uintx CMSTriggerRatio                           = 80 {product}
    java version "1.8.0_221"
    Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
    Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
    
    1. MinHeapFreeRatio
    java -XX:+PrintFlagsFinal -version |grep MinHeapFreeRatio
    uintx MinHeapFreeRatio                          = 0  {manageable}
    java version "1.8.0_221"
    Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
    Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
    

    总结:

    1. CMSInitiatingOccupancyFraction默认值是-1,并不是许多人说的68、92之类的。

    2. CMSInitiatingOccupancyFraction是用来计算老年代最大使用率(_initiating_occupancy)的。大于等于0则直接取百分号,小于0则根据公式来计算。这个_initiating_occupancy需要配合-XX:+UseCMSInitiatingOccupancyOnly来使用。

    3. 不同版本最终得到的_initiating_occupancy不同,归根结底应该是不同版本下的MinHeapFreeRatio值不同。

    总得来说,CMS回收器减少了回收的停顿时间,但是降低了堆空间的利用率。

    关键参数说明

    • -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$LOG_DIR/heap_dump_%p.log

    • -XX:MaxTenuringThreshold=15 年龄

    • -XX:+UseParNewGC

    • -XX:+UseConcMarkSweepGC

    • -XX:CMSInitiatingOccupancyFraction=70 和-XX:+UseCMSInitiatingOccupancyOnly-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC);

    • -XX:+UseCMSInitiatingOccupancyOnly 只是用设定的回收阈值(上面指定的70%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整.

    • -XX:+CMSScavengeBeforeRemark 在CMS GC前启动一次ygc,目的在于减少ygc gen对的old gen引用,降低remark时的开销-----一般CMS的GC耗时 80%都在remark阶段。

    G1

    三色标记算法

    image

    RSET 代际引用

    Rset 是一种抽象概念,记录对象在不同代际之间的引用关系,目的是为了加速垃圾回收的速度。JVM使用根对象引用的收集算法,即从根命令出发,标记所有存活的对象,然后遍历对象的每一个了段继续标记,直到所有的对象标记完成。在分代GC中,我们知道新生代和老年代处于不同的收集阶段,如果还是按照这种标记方法,既不合理也没必要。假设我们只收集年轻代,我们要把老年代全部标记,但是并没有收集老年代,浪费了时间。同理在收集老年代时有同样的问题。当且仅当我们要进行Full GC时,才需要做全部标记。所以算法设计者做了这样的设计,用一个Rset 记录非收集部分指向收集部分的指针集合,而这个集合描述对象的引用关系。

    根据这个思路,我们总结分区之间的引用关系。

    • 分区内部的引用关系

    无论是新生代分区还是老年代内部分区的引用关系都无需记录,因为回收是针对整个分区而言,要么被回收,要么不被回收,回收的时侯会遍历整个分区,所以无需记录这种额外的引用关系。

    • 新生代分区到新生代分区之间的引用关系

    这个也无需记录,原因在于G1的回收算法都会全量处理新生代分区,所以它们会被遍历。

    • 新生代分区到老年代分区之间的引用关系

    这个无需记录,G1会用新生代分区作为根,所以在遍历新生代时自然会找到老年代分区,所以无需记录。

    • 老年代分区到新生代分区之间的引用关系

    这个需要记录,在YGC的时侯有两种根,一个就是栈空间、全局变量的引用,另一个就是老年代分区到新生代分区的引用。

    • 老年代分到老年代分区之间的引用

    这个需要记录,在混合GC的时侯可能只有部分分区被回收,所以必须记录引用关系,快速找到哪些对象是活跃的。

    G1 STAB 解决新创建对象产生的漏标问题

    image

    官方参数说明

    英文原文https://www.oracle.com/technical-resources/articles/java/g1gc.html

    中文文档https://www.oracle.com/cn/technical-resources/articles/java/g1gc.html https://www.cnblogs.com/cuizhiquan/articles/11058497.html

    G1 回收过程如下。

    1. G1 的年轻代回收,采用复制算法,并行进行收集,收集过程会 STW。
    2. G1 的老年代回收时也同时会对年轻代进行回收。主要分为四个阶段:
      2.1 初始标记阶段完成对根对象的标记,这个过程是STW的;
      2.2. 并发标记阶段,这个阶段是和用户线程并发执行的;
      2.3 最终标记阶段,完成三色标记周期(STW)
      2.4 复制/清除阶段,这个阶段会优先对可回收空间较大的 Region 进行回收,即 garbage first,这也是 G1 名称的由来。(STW)

    G1采用每次只清理一部分而不是全部的 Region 的增量式清理,由此来保证每次GC停顿时间不会过长。

    总结如下,G1 是逻辑分代不是物理划分,需要知道回收的过程和停顿的阶段。此外还需要知道,G1 算法允许通过 JVM 参数设置 Region 的大小,范围是 1~32MB,可以设置期望的最大 GC 停顿时间等。

    image image

    https://segmentfault.com/a/1190000022537037

    参数配置

    参数及默认值 说明
    -XX:G1HeapRegionSize = n 设置region的大小,该值为2的幂,范围为1MB到32 MB,目标是根据最小Java堆大小,将堆分成大约2048个region。
    -XX:MaxGCPauseMillis = 200 设置最大停顿时间,默认值为200毫秒。
    -XX:G1NewSizePercent = 5 设置年轻代占整个堆的最小百分比,默认值是5,这是个实验参数,如果设置该值,将覆盖默认参数 -XX:DefaultMinNewGenPercent。(JVM build > 23)
    -XX:G1MaxNewSizePercent = 60 设置年轻代占整个堆的最大百分比,默认值是60,这是个实验参数,如果设置该值,将覆盖默认参数 -XX:DefaultMaxNewGenPercent。(JVM build > 23)
    -XX:ParallelGCThreads = n 设置STW的垃圾收集线程数,当逻辑处理器数量小于8时,n的值与逻辑处理器数量相同;如果逻辑处理器数量大于8个,则n的值大约为逻辑处理器数量的5/8,大多数情况下是这样,除了较大的SPARC系统,其中n的值约为逻辑处理器的5/16。
    -XX:ConcGCThreads = n 设置并行标记线程的数量,设置n大约为ParallelGCThreads参数值的1/4。
    -XX:InitiatingHeapOccupancyPercent = 45 设置触发标记周期的Java堆占用阈值,默认值为45。
    -XX:G1MixedGCLiveThresholdPercent = 65 这个参数表示如果一个分区中的存活对象比例超过n,就不会被挑选为垃圾分区(JVM build > 23)
    -XX:G1HeapWastePercent=10 设置浪费的堆内存百分比,当可回收百分比小于浪费百分比时,JVM就不会启动混合垃圾收。(就是设置垃圾对象占用内存百分比的最大值)。(JVM build > 23)
    -XX:G1MixedGCCountTarget = 8 设置在标记周期完成之后混合收集的数量,以维持old region(也就是老年代)中,最多有G1MixedGCLiveThresholdPercent的存活对象。默认值为8,混合收集的数量将维持在这个值之内。(JVM build > 23)
    -XX:G1OldCSetRegionThresholdPercent = 10 设置在一次混合收集中被收集的old region数量的上线,默认值是整个堆的10%。(JVM build > 23)
    -XX:G1ReservePercent = 10 设置预留空闲内存百分比,以降低内存溢出的风险。默认值为10%。增加或减少百分比时,请确保将总Java堆调整相同的量。(JVM build > 23)

    注:表格中的 JVM build 23表示该参数只有在Java HotSpot VM build 大于 23时才能生效,小于或等于都不能生效

    -XX:+InitiatingHeapOccupancyPercent(简称IHOP):G1内部并行循环启动的设置值,默认为Java Heap的45%。这个可以理解为老年代空间占用的空间,GC收集后需要低于45%的占用率。这个值主要是为了决定在什么时间启动老年代的并行回收循环,这个循环从初始化并行回收开始,可以避免Full GC的发生;

    -XX:G1HeapWastePercent:G1不会回收的内存大小,默认是堆大小的10%。意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。

    -XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。

    G1的回收策略

    1. 新生代老年代是个逻辑的概念,即同一个Region在系统经历过若干次垃圾回收后,做过新也当过老,也有可能新老都没有作过
    2. 空间分配的单位是Region
    3. 虽然新老是个逻辑概念,但他们是有自己的大小上下限的。默认情况下新最大占60%,老最大占(1 - 5%的新生代初始值)=95%
    4. ​触发和回收新生代和之前的过程一样​。不同点在于:由于MaxGCPauseMills的设定,只会在指定时间内回收掉尽可能多的垃圾对象
    5. 进入老年代的条件:
      1. 年龄大了
      2. 动态年龄判断
      3. 存活对象Survivor放不下
      4. 大于单个Region 50%的属于大对象

    GC模式

    G1中提供了三种模式垃圾回收模式,young gc、mixed gc 和 full gc,在不同的条件下被触发。

    • young gc

    发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc,这种触发机制和之前的young gc差不多,执行完一次young gc,活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。

    • mixed gc
      当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。
    • 那么mixed gc什么时候被触发?
      先回顾一下cms的触发机制,如果添加了以下参数:-XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly 当老年代的使用率达到75%时,就会触发一次cms gc。相对的,mixed gc中也有一个阈值参数​-XX:InitiatingHeapOccupancyPercent​,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次mixed gc.
    • full gc 如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc.

    UnlockExperimentalVMOptions

    Error: VM option 'G1NewSizePercent' is experimental and must be enabled via -XX:+UnlockExperimentalVMOptions.

    Error: Could not create the Java Virtual Machine.

    Error: A fatal exception has occurred. Program will exit.

    XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=70

    啥时候用G1

    • 优先原则是从业务先优化,如果业务的数据(存储)和算法无法优化的情况下,可以考虑JVM 调优

    • 有实验表明G1在回收Eden分区的时候,大概每GB需要100ms,如果GC回收时间超过500ms以上,可以考虑用G1!!

    https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html#garbage_first_garbage_collection

    参考资料

    https://crowhawk.github.io/2017/08/15/jvm_3/

    https://stackoverflow.com/questions/39569649/why-is-cms-full-gc-single-threaded?from=timeline&isappinstalled=0

    https://blogs.oracle.com/jonthecollector/our-collectors

    https://plumbr.io/blog/garbage-collection/minor-gc-vs-major-gc-vs-full-gc?from=timeline&isappinstalled=0

    https://juejin.im/post/5ed32ec96fb9a0480659e547

    https://developer.aliyun.com/article/724633

    https://segmentfault.com/a/1190000022537037

    https://plumbr.io/handbook/garbage-collection-algorithms-implementations/concurrent-mark-and-sweep

    http://ifeve.com/jvm-cms-log/

    相关文章

      网友评论

          本文标题:CMS vs G1基本原理总结

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