CMS背景
CMS曾是为低延迟应用设计的垃圾回收器标杆,而G1则是为解决CMS设计局限并面向未来硬件诞生的继任者。它被替换和移除,是Java技术演进的必然结果。
从JDK 9开始,G1已成为JVM的默认垃圾收集器。从JDK14开始相关代码已被删除。但我们也需要了解它的原理以及为何会被替代,以便我们更清晰了解垃圾回收器的技术演进之路
🛠️ CMS垃圾回收器详解:为低延迟而生
CMS全称是Concurrent Mark Sweep,它是一款针对"低延迟"需求设计的并发垃圾收集器。传统的垃圾回收器在执行时,会暂停所有应用线程,即"Stop-The-World"(STW),导致服务响应变慢。CMS的核心思想,就是通过多线程与应用程序并发执行,来大幅缩短甚至消除这种暂停。
CMS 工作流程详解
CMS(Concurrent Mark Sweep)收集器针对老年代,目标是最大化缩短应用暂停时间。其工作流程可拆解为 4 个主要阶段 (外加两个可选预处理阶段)。整体流程是标记-清除算法的并发化实现。
阶段概览
| 阶段 | 动作 | 是否STW | 关键特征 |
|---|---|---|---|
| 1. 初始标记 | 标记GC Roots直接引用的对象 | 是 (短暂暂停) | 仅扫描根对象,速度极快,暂停时间很短 |
| 2. 并发标记 | 从根对象出发遍历对象图 | 否 | 耗时最长,与应用线程并发执行,,因此不产生STW |
| 3. 并发预清理 | 提前处理部分并发标记期间的变化 | 否 | 可选阶段,减少后续重新标记暂停时间 |
| 4. 重新标记 | 修正并发标记期间因用户程序导致的对象引用变化 | 是 (暂停) | 使用SATB或增量更新算法,停顿比初始标记长但可控。暂停时间通常比初始标记长,但仍远小于并发标记。 |
| 5. 并发清理 | 回收被标记为垃圾的对象内存 | 否 | 不整理内存,可能产生碎片。清理过程与用户线程同时进行,不阻塞应用。 |
注:实际JVM实现中还包含"并发中止预清理""最终清理"等子阶段,但核心逻辑如上。
各阶段详细说明
1. 初始标记 (Initial Mark) -- STW
- 任务 :标记所有 GC Roots 能直接引用的对象(例如栈帧中的本地变量、静态变量、JNI引用等)。
- 停顿时间:非常短,仅依赖根对象的数量,与堆大小无关。
- 产出:创建了根对象标记集合,为后续并发遍历提供起点。
2. 并发标记 (Concurrent Mark) -- 与应用线程并发
- 任务 :从初始标记的根对象集合出发,递归遍历整个老年代对象图,标记所有存活对象。
- 特点 :
- 这个阶段耗时最长,但因为是并发的,应用线程仍在运行,GC线程仅占用部分CPU。
- 并发期间,应用线程可能修改对象引用关系(如修改指针、新建对象),导致已经标记的对象状态发生变化。
- 处理并发变化:CMS 采用"增量更新"算法记录并发标记期间引用变更的点,留待重新标记阶段修正。
3. 并发预清理 (Concurrent Preclean) -- 并发(可选但有价值)
- 任务 :这是一个 辅助阶段,并非严格必须。它会尝试提前处理在并发标记阶段通过增量更新记录下来的那些"脏"对象卡表,减少后续重新标记阶段的扫描工作量。
- 目的:因为重新标记需要STW,做一次提前并发清理,可以缩短真正的STW时间。
- 时机:当并发标记阶段基本完成,但还未进入重新标记时执行。
4. 重新标记 (Remark) -- STW
- 任务:修正并发标记期间因应用线程运行而变化的那些对象的标记状态。
- 采用技术 :CMS使用 增量更新 算法。在并发阶段,当应用线程修改一个已经被标记过的对象引用时,JVM会记录该对象(将其放入一个队列)。重新标记时,只扫描这些"脏对象"及其直接引用链,而不是整个堆。
- 停顿时间:通常比初始标记长,但远小于一次Full GC。如果并发预清理阶段效果显著,停顿会很短。
- 额外处理:还会处理新生代晋升对象、跨代引用等(需要扫描新生代或使用卡表)。
5. 并发清理 (Concurrent Sweep) -- 并发
- 任务:回收所有在标记阶段被判定为不可达的"垃圾"对象所占用的内存空间。
- 并发特点:清理线程与应用线程同时进行,不阻塞应用。
- 内存布局结果 :由于是"清除"而非"复制/整理",会导致老年代出现内存碎片。这是CMS被G1取代的重要原因之一。
6. 并发重置 (Concurrent Reset) -- 并发
- 任务:清理CMS内部数据结构,为下一次回收做准备。
特殊情况:Concurrent Mode Failure
如果并发回收的速度跟不上对象分配的速度(比如老年代急速增长),CMS可能遇到 并发模式失败。此时JVM会采取降级措施:
- 暂停所有应用线程。
- 使用单线程的Serial Old收集器 进行一次全停顿的老年代整理(标记-整理算法)。
- 这次Full GC通常停顿时间极长,严重损害系统性能。
总结:CMS的完整流程图示
text
[初始标记] --STW--> [并发标记] --> [并发预清理] --> [重新标记] --STW--> [并发清理] --> [并发重置]
短暂停 并发长耗时 并发 中短暂停 并发 并发
光环下的阴影:CMS的三大核心缺陷
尽管CMS极大地改善了GC暂停问题,但随着硬件和应用的进化,其缺点也变得愈发突出:
- 内存碎片化 :CMS基于
Mark-Sweep算法,不进行内存整理,会逐渐产生无法被利用的内存"碎片"。这可能导致明明总内存足够,却因找不到连续 空间分配给大对象而频繁触发Full GC,最终引发内存溢出错误OutOfMemoryError。 - Concurrent Mode Failure :CMS在并发回收时,应用程序会继续创建对象。如果并发期间预留内存不足,回收速度跟不上分配速度,就会发生"并发模式失败"。作为后备方案,JVM会暂停所有线程,启用单线程的
Serial Old进行全局整理,此时GC停顿时间将会骤增。 - 对CPU资源敏感:虽然并发阶段不暂停应用线程,但GC线程会消耗宝贵的CPU资源(CMS默认启动(CPU核心数+3)/4个线程)。在核心数较少的环境下,可能导致应用吞吐量显著下降。
简单说:CMS为了低延迟牺牲了稳定性和可预测性,而G1(及后续ZGC)通过分区、整理和停顿预测模型,在保证低延迟的同时彻底规避了这两个问题
🤔 为何被G1取代:继任者的全面超越
G1(Garbage-First)在设计之初就被官方定位为CMS的长期替代者。它通过全新的设计思想,从根本上解决了CMS的老大难问题:
- 变革性的Region分区设计:G1将堆内存划分为多个大小相等的独立区域(Region),且每个Region可以动态扮演Eden、Survivor或Old区中的任意角色。
- 消灭内存碎片 :G1从整体上采用
Mark-Compact(标记-整理)算法,局部采用Mark-Copy(标记-复制)。其在回收Region时会执行"疏散"(Evacuation),将存活对象复制到新的空闲Region,从而保证内存始终是紧密排列、无碎片的。 - 可控、可预测的停顿 :G1不追求每次都回收全部内存,而是建立一个"停顿预测模型"。用户可以设定期望的最大GC暂停时间(例如
-XX:MaxGCPauseMillis=200),G1会优先选择垃圾最多、清理收益最大的Region进行回收,在满足此目标的前提下实现吞吐量最大化。
💎 移除时间线:从废弃到告别
CMS的退场是一个循序渐进的过程:
- JDK 9 :CMS被正式标记为废弃(Deprecated),表明官方不再推荐使用,并计划在未来版本中移除。从JDK 9开始,G1已成为JVM的默认垃圾收集器。
- JDK 14 :Oracle通过 JEP 363 彻底移除了CMS垃圾收集器的源代码和相关选项。从该版本开始,在JVM参数中指定
-XX:+UseConcMarkSweepGC将会被JVM直接忽略,并回退到使用默认的G1。