JVM垃圾收集算法-CMS

版权声明:非商业目的可自由转载,转载请标明出处 https://blog.csdn.net/u010013573/article/details/83965503

一、概述

    CMS为老年代的并发收集器,即在对老年代进行gc时,与用户线程并发执行;基于标记-清除算法实现,对于大于3G小于8G的堆较合适。gc过程分为:初始标记(其中标记为标记活动的对象,没有被标记的则表示可回收的对象),并发标记,重新标记,并发清除和并发重置五个阶段,其中初始标记,重新标记会出现STW,如下gc日志为显示这两种标记导致应用停顿的时间:

1f076ffb0f6251e917fbcf8af19ea4ec05c.jpg        可通过:cat gc.log | grep -A 1 "CMS Final Remark",进一步查看本次CMS造成应用实际停顿的时间。如下:

882947fcc59803c3d07c2cb335420c4cfdc.jpg

 二、CMS对老年代进行垃圾收集的详细过程

        1. 初始标记initMark:采用可达性分析,在老年代堆中标记GC ROOTS可以直接到达的对象,所以很快;

        2. 并发标记:以初始标记中可达的对象为起点,标记所有可达对象,此过程与用户线程并发执行。之所以将标记分为初始标记和并发标记,而不是在一次标记里面完成,是因为通过在初始标记只标记从GC ROOTS直接可达的对象,而不继续往下标记,这样可以减少STW的时间,在并发标记时,再继续往下标记,此时与用户线程并发执行,不会造成停顿;

        3. 重新标记Remark

        (1)在并发标记阶段,用户线程在并发执行,所以此时可能出现某些对象从初始标记阶段时的不可达,在并发标记时,用户线程更新而变成可达,所以需要进行重新标记来避免这些对象被清理。这个阶段由于存在”跨代引用“,即新生代引用老年代,所以该阶段是从新生代开始的,所以涉及整个堆的扫描来确定哪些对象还存活。所以如果此时新生代存在较多存活对象,该阶段就会存在较长耗时。

       (2)由于重新标记会扫描整个堆中的对象,包括新生代和老年代,为了缩短该阶段造成的STW,CMS会在该Remark之前,进行一次可中断的并发预清理(CMS-concurrent-abortable-preclean):

        并发预清理:与用户线程并发执行,功能是将新生代中在并发标记过程中晋升到老年代的对象清除,从而减少Remark所需扫描对象的数量。具体发生与否要看Eden的大小是否大于2M,这是默认值,可以修改。增加这个阶段的好处如果在这个阶段发生了MG,则此时新生代的大小就会变小,这样重新标记所需扫描对象就会减少,减少造成应用STW的时间。而CMS-concurrent-abortable-preclean也不是无限进行的,通过参数CMSMaxAbortablePrecleanTime,默认为5s,在CMS-concurrent-abortable-preclean执行超过5s时,则直接进入重新标记阶段。

      (3)除了以上所说优化外,CMS还可以通过设置CMSScavengeBeforeRemark参数,强制Remark前执行一次MG,从而回收新生代中对象,减少新生代中需要扫描的对象。

        4. 并发清除:并发清理年老代中除以上标记还存活对象之外的其他对象,此过程不会造成应用STW,同时也不会对内存进行压缩整理,会出现内存碎片。清除后,CMS不是简单地用一根指针执行未分配的空间,而是汇总到一个列表当中,当需要分配内存时,从该列表中寻找能放下该对象的内存空间。

        5. 并发重置:与CMS相关的数据结构并重新初始化,准备下一次CMS收集。

        整个标记清理过程如图:

b687c455c80137b407993e385ec7992363c.jpg

       各阶段线程分布于执行情况:

5eaac09162c4c60c004c7055981d34ffd0e.jpg

三、拓展思考

    1. 何时压缩老年代空间:CMS使用的是标记清除算法,不会对老年代堆进行压缩,而CMS是属于Major GC,在Major GC后仍旧无法容纳对象时,则发生Full GC, Full GC会对老年代堆进行压缩。

    2. 如果判断CMS算法效果:主要查看gc日志的初始标记initial mark和重新标记final remark的real耗时,这两个阶段会造成STW。同时整体效果需要查看stopped的时间,根据这个判断是否达到应用需求,如上面截图。

    3. 新生代垃圾收集器:CMS只能与ParNew新生代垃圾收集器一起使用,开启为:-XX:+UseParNewGC,也可以不加该参数,CMS默认使用ParNew作为新生代垃圾收集器。

    4.什么情况下CMS比较适合:

    (1)响应时间优先,能接受牺牲一定的吞吐量,如果需要高响应时间和高吞吐量,推荐使用G1。后续文章再继续介绍G1。G1也是JDK9默认的垃圾收集器;

    (2)硬件配置较高,即CPU和内存资源充足。CMS由于需要与用户线程并发执行,所以可能会竞争CPU资源。同时CMS并发标记阶段,用户线程同时执行时会新建对象,故内存占用会比较高;

    (3)堆大小在3G到8G之间,同时存活时间较长的对象比较多。

推荐阅读:

https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html

https://tech.meituan.com/jvm_optimize.html

223916_bL9y_2663968.jpg

猜你喜欢

转载自blog.csdn.net/u010013573/article/details/83965503