(六)详解CMS
CMS 的全称是 Mostly Concurrent Mark and Sweep Garbage Collector(主要并发标记清除垃圾收集器),它在年轻代使⽤复制算法,⽽对⽼年代使⽤标记-清除算法。你可以看到,在⽼年代阶段,⽐起 Mark-Sweep,它多了⼀个并发字样。
CMS 的设计⽬标,是避免在⽼年代 GC 时出现长时间的卡顿(但它并不是⼀个⽼年代回收器)。如果你不希望有长时间的停顿,同时你的CPU 资源也⽐较丰富,使⽤ CMS 是⽐较合适的。
CMS 使⽤的是 Sweep(清除) ⽽不是 Compact(复制),所以它的主要问题是碎⽚化。随着 JVM 的长时间运⾏碎⽚化会越来越严重,只有通过 Full GC 才能完成整理。
CMS 回收过程
1.初始标记(Initial Mark)
初始标记阶段,只标记直接关联 GC root 的对象,不⽤向下追溯。因为最耗时的就在 tracing 阶段,这样就极⼤地缩短了初始标记时间。
这个过程是 STW 的,但由于只是标记第⼀层,所以速度是很快的。
注意,这⾥除了要标记相关的 GC Roots 之外,还要标记年轻代中对象的引⽤,这也是 CMS ⽼年代回收,依然要扫描新⽣代的原因。2.并发标记(Concurrent Mark)
在初始标记的基础上,进⾏并发标记。这⼀步骤主要是 tracinng 的过程,⽤于标记所有可达的对象。
lycee这个过程会持续⽐较长的时间,但却可以和⽤户线程并⾏。在这个阶段的执⾏过程中,可能会产⽣很多变化:有些对象,从新⽣代晋升到了⽼年代;
有些对象,直接分配到了⽼年代;
u的用法
⽼年代或者新⽣代的对象引⽤发⽣了变化。
上节中提到过的的卡⽚标记,在这个阶段受到影响的⽼年代对象所对应的卡页,会被标记为 dirty,⽤于后续重新标记阶段的扫描。
并发预清理(Concurrent Preclean)
本阶段也是不会 STW,⽬的是为了让重新标记阶段的 STW 尽可能短。这时候⽼年代中被标记为 dirty 的卡页中的对象,就会被重新标记,然后清除掉 dirty 的状态。
由于这个阶段也是可以并发的,在执⾏过程中引⽤关系依然会发⽣⼀些变化。我们可以假定这个清理动作是第⼀次清理。
所以重新标记阶段,有可能还会有处于 dirty 状态的卡页。考研英语复习经验
并发可取消的预清理(Concurrent Abortable Preclean)stylin
因为重新标记是需要 STW 的,所以会有很多次预清理动作。并发可取消的预清理,顾名思义,在满⾜某些条件的时候,可以终⽌,⽐如迭代次数、有⽤⼯作量、消耗的系统时间等。
这个阶段是可选的。换句话说,这个阶段是“并发预清理”阶段的⼀种优化。
这个阶段的第⼀个意图,是避免回扫年轻代的⼤量对象;另外⼀个意图,就是当满⾜最终标记的条件时,⾃动退出。
我们在前⾯说过,标记动作是需要扫描年轻代的。如果年轻代的对象太多,肯定会严重影响标记的时间。如果在此之前能够进⾏⼀次 Minor GC,情况会不会变得好了许多?
风月俏佳人插曲CMS 提供了参数 CMSScavengeBeforeRemark,可以在进⼊重新标记之前强制进⾏⼀次 Minor GC。
elong
但请你记住⼀件事情,GC 的停顿是不分什么年轻代⽼年代的。设置了上⾯的参数,可能会在⼀个⽐较长的 Minor GC 之后,紧跟着⼀个CMS 的 Remark,它们都是 STW 的。
这部分有⾮常多的配置参数。但是⼀般都不会去改动。
3.最终标记(Final Remark)
通常 CMS 会尝试在年轻代尽可能空的情况下运⾏ Final Remark 阶段,以免接连多次发⽣ STW 事件。
这是 CMS 垃圾回收阶段的第⼆次 STW 阶段,⽬标是完成⽼年代中所有存活对象的标记。我们前⾯多轮的 preclean 阶段,⼀直在和应⽤线程玩追赶游戏,有可能跟不上引⽤的变化速度。本轮的标记动作就需要 STW 来处理这些情况。
如果预处理阶段做的不够好,会显著增加本阶段的 STW 时间。你可以看到,CMS 垃圾回收器把回收过程分了多个部分,⽽影响最⼤的不是 STW 阶段本⾝,⽽是它之前的预处理动作。
4.并发清除(Concurrent Sweep)
此阶段⽤户线程被重新激活,⽬标是删掉不可达的对象,并回收它们的空间。
由于 CMS 并发清理阶段⽤户线程还在运⾏中,伴随程序运⾏⾃然就还会有新的垃圾不断产⽣,这⼀部分垃圾出现在标记过程之后,CMS ⽆法在当次 GC 中处理掉它们,只好留待下⼀次 GC 时再清理掉。这⼀部分垃圾就称为“浮动垃圾”。
财务通则并发重置(Concurrent Ret)
此阶段与应⽤程序并发执⾏,重置 CMS 算法相关的内部数据,为下⼀次 GC 循环做准备。
内存碎⽚
由于 CMS 在执⾏过程中,⽤户线程还需要运⾏,那就需要保证有充⾜的内存空间供⽤户使⽤。如果等到⽼年代空间快满了,再开启这个回收过程,⽤户线程可能会产⽣“Concurrent Mode Failure”的错误,这时会临时启⽤ Serial Old 收集器来重新进⾏⽼年代的垃圾收集,这样停顿时间就很长了(STW)。
这部分空间预留,⼀般在 30% 左右即可,那么能⽤的⼤概只有 70%。参数 -XX:CMSInitiatingOccupancyFraction ⽤来配置这个⽐例(记得要⾸先开启参数UCMSInitiatingOccupancyOnly)。也就是说,当⽼年代的使⽤率达到 70%,就会触发 GC 了。如果你的系统⽼年代增长不是太快,可以调⾼这个参数,降低内存回收的次数。
其实,这个⽐率⾮常不好设置。⼀般在堆⼤⼩⼩于 2GB 的时候,都不会考虑 CMS 垃圾回收器。
另外,CMS 对⽼年代回收的时候,并没有内存的整理阶段。这就造成程序在长时间运⾏之后,碎⽚太多。如果你申请⼀个稍⼤的对象,就会引起分配失败。
CMS 提供了两个参数来解决这个问题:
(1) UCMSCompactAtFullCollection(默认开启),表⽰在要进⾏ Full GC 的时候,进⾏内存碎⽚整理。内存整理的过程是⽆法并发的,所以停顿时间会变长。
(2)CMSFullGCsBeforeCompaction,每隔多少次不压缩的 Full GC 后,执⾏⼀次带压缩的 Full GC。默认值为 0,表⽰每次进⼊ Full GC 时都进⾏碎⽚整理。
所以,预留空间加上内存的碎⽚,使⽤ CMS 垃圾回收器的⽼年代,留给我们的空间就不是太多,这也是 CMS 的⼀个弱点。
⼩结
⼀般的,我们将 CMS 垃圾回收器分为四个阶段:
1. 初始标记
2. 并发标记
3. 重新标记
leave过去式4. 并发清理
我们总结⼀下 CMS 中都会有哪些停顿(STW):
1. 初始标记,这部分的停顿时间较短;
2. Minor GC(可选),在预处理阶段对年轻代的回收,停顿由年轻代决定;
3. 重新标记,由于 preclaen 阶段的介⼊,这部分停顿也较短;
4. Serial-Old 收集⽼年代的停顿,主要发⽣在预留空间不⾜的情况下,时间会持续很长;
5. Full GC,永久代空间耗尽时的操作,由于会有整理阶段,持续时间较长。
再来看⼀下 CMS 的 trade-off(权衡)。
优势:
低延迟,尤其对于⼤堆来说。⼤部分垃圾回收过程并发执⾏。
劣势:
きみはぐ1. 内存碎⽚问题。Full GC 的整理阶段,会造成较长时间的停顿。
2. 需要预留空间,⽤来分配收集阶段产⽣的“浮动垃圾”。
邀请函英文3. 使⽤更多的 CPU 资源,在应⽤运⾏的同时进⾏堆扫描。
CMS 是⼀种⾼度可配置的复杂算法,因此给 JDK 中的 GC 代码库带来了很多复杂性。由于 G1 和 ZGC 的产⽣,CMS 已经在被废弃的路上。但是,⽬前仍然有⼤部分应⽤是运⾏在 Java8 及以下的版本之上,针对它的优化,还是要持续很长⼀段时间。