核心目标与设计理念
CMS(Concurrent Mark-Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合那些需要与用户交互的互联网或B/S架构的服务端应用,良好的响应速度能提升用户体验。
CMS的设计理念是:让垃圾收集线程与用户线程(大部分时间)同时工作。它并非追求单次GC的速度最快,而是通过并发,将原本一个长时间的“Stop-The-World”停顿,拆分成多个极短的停顿和长时间的并发操作,从而从整体上降低应用的停顿时间。
CMS的执行过程
CMS的运作过程比之前的收集器更复杂,分为以下四个核心步骤:
1. 初始标记(Initial Mark)
- 任务:仅仅标记一下GC Roots能直接关联到的对象(即第一层对象)。这个过程速度极快。
- 状态:需要 “Stop The World” (STW)。但因其只标记直接关联的对象,所以停顿时间非常短。
2. 并发标记(Concurrent Mark)
- 任务:从“初始标记”阶段标记的对象开始,进行可达性分析,遍历整个对象图。这个过程耗时较长。
- 状态:并发执行。垃圾收集线程与用户线程一起工作。这是CMS实现低延迟的关键。
3. 重新标记(Remark)
- 任务:修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。(具体原因见下文“三色标记法”)。
- 状态:需要 “Stop The World” (STW)。这个阶段的停顿时间通常会比初始标记阶段稍长,但远低于并发标记的时间。通过多种优化手段(如增量更新、预清理),可以缩短此时间。
4. 并发清除(Concurrent Sweep)
- 任务:清理删除掉标记阶段判断的已经死亡的对象,释放其占用的空间。
- 状态:并发执行。垃圾收集线程与用户线程一起工作。
下图直观地展示了CMS收集器四个阶段的工作流程及其与用户线程的并发/STW关系:
核心技术:三色标记法与增量更新
CMS的并发标记阶段之所以能安全地与用户线程并发,其理论基础是三色标记法(Tri-color Marking)。它将所有对象分为三种颜色:
- 白色:表示对象尚未被垃圾收集器访问过。在可达性分析开始时,所有对象都是白色。在分析结束后,仍然是白色的对象,即代表不可达,会被回收。
- 黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经被扫描过。黑色对象是安全存活的,如果有其他引用指向了黑色对象,无需重新扫描。黑色对象不可能直接(不经过灰色对象)指向白色对象。
- 灰色:表示对象已经被垃圾收集器访问过,但这个对象至少还有一个引用没有被扫描过。灰色对象是分析过程的中间状态。
并发标记时,收集器从灰色对象集合中取出对象开始扫描。扫描的过程就是将灰色对象涂黑,并将其直接引用的白色对象涂灰的过程。
并发标记带来的问题:在收集器标记的过程中,用户线程同时在修改引用关系,这会导致两种主要问题:
- 原本消亡的对象被误标为存活(浮动垃圾):可以容忍,下次GC再清理。
- 原本存活的对象被误标为消亡(对象消失):这是严重bug,绝对不允许发生。
对象消失需要同时满足以下两个条件:
- 条件一:赋值器插入了一条或多条从黑色对象到白色对象的新引用。
- 条件二:赋值器删除了全部从灰色对象指向该白色对象的直接或间接引用。
为了解决“对象消失”问题,CMS在重新标记阶段使用了 增量更新(Incremental Update) 算法。
- 原理:当黑色对象插入了新的指向白色对象的引用时(破坏条件一),CMS会将这个插入操作记录下来(JVM使用写屏障实现)。在重新标记阶段,会将这些记录过的黑色对象重新变为灰色对象,然后以这些灰色对象为根,重新扫描一次。这样可以确保那些新被黑色对象引用的白色对象不会被错误回收。
- 简单理解:“无论引用关系删除与否,都会按照刚刚开始扫描的那一刻的对象图快照来进行搜索”。
卡表(Card Table)的作用
为了支持高效的重新标记,避免在重新标记阶段重新扫描整个老年代,HotSpot虚拟机使用了卡表(Card Table) 技术。
- 是什么:卡表是一个字节数组,其中的每个元素对应着老年代内存的一块特定大小的内存块(通常为512字节),这个内存块称为“卡页”(Card Page)。
- 如何工作:如果老年代中的一个对象引用了一个新生代中的对象(这种跨代引用在分代收集中很常见),JVM就会通过写屏障技术,将对应卡页的卡表元素标记为脏(Dirty)。
- 在CMS中的应用:在重新标记阶段,除了遍历GC Roots,只需要遍历卡表中被标记为“脏”的区域,而不用扫描整个老年代。这大大缩短了重新标记阶段的停顿时间。
CMS的优缺点与调优
优点
- 并发收集:大部分GC工作(标记和清除)与用户线程并发执行,极大地降低了停顿时间。
缺点与解决方案
- 对处理器资源非常敏感:
- 问题:在并发阶段,虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,总吞吐量降低。CMS默认的回收线程数是 (CPU核心数 + 3) / 4,当CPU核心数不足4个时,CMS对用户程序的影响可能变得很大。
- 解决方案:提供足够的CPU资源。
- 无法处理“浮动垃圾”(Floating Garbage):
- 问题:在并发清除阶段,用户线程还在运行,自然还会产生新的垃圾。这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只能留到下一次GC时再清理。这就是“浮动垃圾”。
- 后果:因此,CMS不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运行使用。如果预留的内存无法满足程序分配新对象的需要,就会出现一次 “并发失败”(Concurrent Mode Failure),这时虚拟机将不得不启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就会很长。
- 解决方案:使用 -XX:CMSInitiatingOccupancyFraction 参数来设定老年代空间的使用阈值。JDK 5默认值为68%,JDK 6默认值为92%。可以根据实际情况调低此值,为浮动垃圾预留足够空间。
- 内存碎片问题:
- 问题:CMS是基于 “标记-清除” 算法的收集器,这意味着收集结束后会产生大量的内存碎片。
- 后果:当无法找到足够大的连续空间来分配当前对象时,不得不提前触发一次Full GC。
- 解决方案:
- 使用 -XX:+UseCMSCompactAtFullCollection 参数(默认开启),在Full GC时开启内存碎片的合并整理过程。但这个整理过程是STW的。
- 使用 -XX:CMSFullGCsBeforeCompaction 参数(默认值为0),用于设定执行多少次不整理空间的Full GC后,跟着来一次带整理的。
总结
CMS是一款划时代的老年代收集器,它首次实现了GC线程与用户线程的大规模并发工作,极大地降低了延迟。然而,它并非一款“全能”收集器,其并发失败和内存碎片的缺点在堆内存巨大、对象分配频繁的应用中会显得尤为突出。随着官方将发展重心转移到更加先进的G1、ZGC等收集器上,CMS已在JDK 9中被标记为废弃(Deprecated),并在JDK 14中被移除(JEP 363)。但理解其工作原理,特别是三色标记法和卡表等技术,对于理解所有现代并发收集器(如G1)至关重要。