CMS 垃圾收集器,详细说明

482 阅读3分钟

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

重头戏来了。CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它是一种多线程的”标记-清除算法“收集器。目前主流 Java 应用的服务器端使用的都是这种收集器,在尤其重视服务的相应速度的场景下可以考虑使用 CMS 收集器。

CMS 的收集过程主要有下面四个步骤:

  • 初始标记(CMS initial mark)

    初始标记主要是枚举根节点,这一步需要 STW。不过这一步仅仅标记 GC Roots,所以时间很短。

  • 并发标记(CMS concurrent mark)

    并发标记阶段主要进行可达性分析,这一步不需要 STW,所以 GC 线程是与用户线程并行的。

  • 重新标记(CMS remark)

    这已阶段主要是修正并发标记时对象的引用关系发生变化的部分。需要 STW,时间也比初始标记阶段长一点。幸运的是这一过程是多线程并行的。

  • 并发清除(CMS concurrent sweep)

    清除标记的对象,这一步也不需要 STW,GC 线程与用户线程是并行的。

CMS-收集器

CMS 收集器虽然通过并发的技术,降低了用户线程的停顿时间,但是也有一些不足。比如最明显的一个缺点就是,它使用的是”标记-清除算法“,如果你还记得这种算法,就应该知道,它有一个最明显的问题就是会产生大量的内存碎片。往往老年代明明有很大的空间,但是无法找到连续的空间分配对象,不得不提前触发 Full GC。

为了解决这个问题, CMS 提供了 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启)用于开启内存碎片的整理过程,遗憾的是,这个整理过程依然需要 STW,内存碎片没有了,但是停顿时间也变长了。JVM 还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction 这个参数可以设置一个整数值,表示执行多少次 Full GC 后进行一次碎片整理,默认为 0 (表示每次 Full GC 时都会进行碎片整理)。

由于 CMS 收集器是与用户线程并行的,所以带来的另一个问题就是占用了部分 CPU 的资源,导致应用程序变慢。目前,CMS 默认启动的回收线程数是(CPU数量+3)/4,也就是说,如果你的 CPU 个数 小于 4,可能对用户线程影响较大。但是对于大型的 Java 应用程序部署的服务器,动辄十几甚至几十个 CPU 来说,这个问题带来的影响越来越小。

CMS 最后一个问题出现在并发清理的过程。由于程序是一直在运行,所以在清理过程中也会产生垃圾。这部分垃圾出现在标记之后,所以没办法在这一次的 GC 过程中清理掉,只能留在下次 GC 时清理。

由于在垃圾收集阶段,有用户线程在运行,所以不能等到老年代的内存被填满之后再进行 GC,需要剩余一部分空间用于在 GC 期间的对象创建。可以通过 -XX:CMSInitiatingOccupancyFraction 参数来设置一个百分比,当老年代的使用率达到多少时触发 GC,默认值是 92%,就是说当老年代的使用率超过这个值就会触发 GC。

如果在 GC 期间,剩余的内存不足以满足用户线程的需要,那么就会发生“Concurrent Mode Failure"失败,这时 JVM 就会临时启用 Serial Old 收集器来做垃圾收集,同时,STW 的停顿时间也会变长。所以,如果 CMSInitiatingOccupancyFraction 参数设置的过高就会触发大量的”Concurrent Mode Failure“出现。