美文网首页
6、CMS垃圾回收器的详解

6、CMS垃圾回收器的详解

作者: 后山野鹤 | 来源:发表于2020-03-04 21:26 被阅读0次

    本次主要学习下上次讲到的CMS垃圾回收器,更加深入的学习下垃圾回收的过程。
    先抛出几个新的概念:
    1.Minor GC:发生在年轻代的GC
    2.Major GC:发生在老年代的GC
    3.Full GC:全堆垃圾回收。比如Metaspace区引起年轻代和老年代的回收
    CMS的全称是Mostly Concurrent Mark and Sweep Garbage Collector(主要并发标记清除垃圾回收器),它在年轻代使用复制算法,在老年代使用标记-清除算法,在老年代比起Mark-Sweep,多了一个并发设计。
    CMS的设计目标是避免在老年代GC时出现长时间的卡顿。如果你不希望有长时间的卡顿,同时你的CPU资源也比较丰富,使用CMS是比较合适的。
    CMS使用的是Sweep而不是Compact,所以它的主要问题是碎片化问题。随着JVM的长时间运行,碎片化会越来越严重,只有通过GC Full才能完成整理。
    为什么CMS能够获得更小的停顿时间呢?
    主要是因为它把最耗时的一些操作,做成了和应用线程并行。

    CMS回收过程

    初始标记(Initial Mark)

    初始标记阶段,只标记直接关联的GC Root 的对象,不用向下追溯。因为最耗时的就在tracing阶段,这样就极大地缩短了初始标记时间。
    这个过程是STW的,但由于只是标记第一层,所以速度是很快的。


    第一层标记

    注意,这里除了要标记相关的GC Roots之外,还要标记年轻代中对象的引用,这也是CMS老年代回收,依然要扫描新生代的原因。

    并发标记(Concurrent Mark)

    在初始标记的基础上,进行并发标记。这一步骤主要是tracing的过程,用于标记所有可达的对象。
    这个过程会持续比较长的时间,但却可以和用户线程并行。在这个阶段的执行过程中,可能会产生很多变化:
    1.有些对象,从新生代晋升到了老年代;
    2.有些对象,直接分配到了老年代;
    3.老年代或者新生代的对象引用发生了变化;


    并发标记

    之前提到过卡片标记,在这个阶段受到影响的老年代对象对应的卡页,会被标记为dirty,用于后续重新标记阶段的扫描。

    并发预清理(Concurrent Preclean)

    并发预清理也是不需要STW的,目的是为了让重新标记阶段的STW尽可能短。这个时候老年代中被标记为dirty的卡页中的对象,就会被重新标记,然后清除掉dirty的状态。
    由于这个阶段是并发的,在执行过程中引用关系依然会发生一些变化

    并发可取消的预清理(Concurrent Abortable Preclean)

    因为重新标记是需要STW的,所以会有很多次预清理动作。并发可取消的预清理,顾名思义,在满足某些条件下的时候,可以终止,比如迭代次数,有用工作量、消耗的系统时间等。
    这个阶段是可选的。换句话说,这个阶段是并发预清理阶段的一种优化。
    这个阶段的第一个意图是,避免回扫年轻代的大量对象;另一个意图就是当满足最终目标的条件时,自动退出。
    前面提到,标记动作是需要扫描年轻代的。如果年轻代的对象太多,肯定会严重影响标记的时间。如果在此之前能够进行一次并行Minor GC,情况会不会变得好了很多?
    CMS提供了参数CMSScavengeBeforeRemark,可以在进入重新标记之前强制进行一次Minor GC。
    请记住一件事情,GC的停顿时间是不分什么年轻代老年代。设置了什么的参数,可能会在一个比较长的Minor GC之后,紧跟着一个CMS的Remark,它们都是STW的。这部分有很多参数,但一般都不会去改动。

    最终标记(Final Remark)

    通常CMS会尝试在年轻代尽可能对象少的情况下运行Final Remark阶段,以免接连多次发生STW事件。
    这是CMS垃圾回收阶段的第二次STW阶段,目标是完成老年代中所有存活对象的标记。前面多轮的preclean阶段,一直在和应用程序玩追赶游戏,有可能跟不上引用的变化速度。本轮的标记动作就需要STW来处理这些情况。
    如果预处理阶段做的不够好,会显著增加本阶段的STW时间。CMS垃圾回收器把回收过程分了多个部分,而影响最大的不是STW阶段本身,而是它之前的预处理动作。

    并发清除(Concurrent Clean)

    此阶段用户线程被重新激活,目标是删除不可达的对象,并回收它们的空间。
    由于CMS并发清理阶段用户线程还在运行中,伴随程序运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在档次GC中清理掉它们,只好留待下一次GC时再清理掉。这一部分被称为浮动垃圾。


    浮动垃圾

    并发重置(Concurrent Reset)

    此阶段与应用程序并发执行,重置CMS算法相关的内部数据,为下一次GC循环做准备。

    内存碎片

    由于CMS在执行过程中,用户线程还需要运行,那就需要保证有充足的内存空间供用户使用。如果等到老年代空间快满了,再开启这个回收过程,用户线程可能会产生"Concurrent Mode Failure"的错误,这时会临时启用Serial Old回收器来重新进行老年代的垃圾回收。这样停顿的时间就很长了(STW)。
    这部分空间预留,一般在30%左右,那么能用的大概70%。参数-XX:CMSInitialtingOccupancyFraction用来配置这个比例(记得要首先开启参数UseCMSInitiatingOccupancyOnly)。也就是说,当老年代的使用率达到70%,就会触发GC了。如果你的系统老年代增长不是太快,可以调高这个参数,降低内存回收的次数。
    其实这个值非常不好设置。一般堆在2G以下的时候都不会考虑使用CMS垃圾回收器。
    另外,CMS回收器对老年代回收的时候,并没有内存的整理阶段,这就造成了程序长时间运行之后,碎片太多,无法申请稍微大点的对象。
    CMS提供了两个参数来解决这个问题
    (1)UseCMSComPactAtFullCollection(默认开启),表示在要进行FullGC的时候,进行内存碎片整理。内存整理的过程是无法并行的,所以停顿时间会变长。
    (2)CMSFullGCsBeforeCompaction,每隔多少次不压缩的Full GC后,执行一次带压缩的Full GC。默认是0,表示每次进行FullGC前,都进行碎片整理。
    所以预留空间加上内存的碎片,使用CMS垃圾回收器的老年代,留给我们的空间就不是,这也是CMS的一个弱点。


    空间预留

    结束语

    一般的,我们将CMS垃圾回收器分为四个阶段:

    1.初始标记
    2.并发标记
    3.重新标记
    4.并发清理

    CMS导致的停顿有以下

    1.初始标记 停顿标记时间较短,只标记GC Roots
    2.Minor GC(可选) 在预处理阶段对年轻代的回收,停顿由年轻代的活跃对象决定
    3.重新标记,由于preclean阶段的介入,这部分停顿也较短
    4.Serial Old 收集老年代的停顿,主要发生在预留空间不足的情况下,时间会持续很长
    5.FullGC ,永久代空间耗尽时的操作,由于会有整理阶段,持续时间较长

    在发生GC问题时,你一定要明确发生在那个阶段,然后对症下药,gclog通常能够非常详细的表现整个过程

    优势

    低延迟,尤其对于大堆来说,大部分垃圾回收过程并发。

    劣势

    1.内存碎片问题,Full GC的整理阶段,会造成较长时间的停顿
    2.需要预留空间,用来分配收集阶段产生的浮动垃圾
    3.使用更得CPU资源,在应用运行的同时进行堆扫描

    CMS是高度可配置的复杂算法,因此给JDK中的GC代码库带来了很多复杂性。由于G1和ZGC的产生,CMS已经在被废弃的路上了。但是,目前仍然有大部分应用是运行在Java8及以下的版本上,针对它的优化,还是要持续很长一段时间。

    相关文章

      网友评论

          本文标题:6、CMS垃圾回收器的详解

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