CMS收集器的三个阶段核心目标是缩短老年代回收停顿时间而非提升整体速度,采用标记-清除算法且不移动对象;初始标记仅扫描GC Roots和年轻代直连的老年代对象,STW但极快;并发标记与用户线程并行,通过写屏障和卡表记录变动以应对漏标;重新标记仅处理脏卡区域,STW可控,确保准确性。

CMS 收集器的这三个阶段,核心目标是把老年代回收的停顿时间拆得尽可能短,而不是追求整体回收更快。它不移动对象,靠标记-清除算法工作,所以关键在于“怎么标得准、停得少”。
初始标记:只扫根和年轻代直连对象,STW 但极快
这个阶段必须暂停用户线程(STW),但只做两件事:
- 从 GC Roots(如虚拟机栈局部变量、方法区静态字段、JNI 引用)出发,标记直接引用的老年代对象
- 再扫描整个年轻代,把其中存活对象所引用的老年代对象也一并标上
它不递归遍历、不扫描全堆,只处理“第一层”可达关系。哪怕堆有几十个 G,通常也只耗几毫秒。多线程标记在 JDK 8+ 可通过 -XX:+CMSParallelInitialMarkEnabled 开启。
并发标记:边跑业务边遍历,耗时最长但不卡用户
从初始标记的结果出发,GC 线程和用户线程同时运行,遍历所有可达对象并打标记。这个阶段不暂停应用,用户无感知,但存在风险:
立即学习“Java免费学习笔记(深入)”;
- 业务线程可能修改引用(比如把 A 指向 B 改成指向 C),导致某些本该活的对象没被标上 → 漏标
- CMS 用写屏障(Write Barrier)配合卡表(Card Table)记录这些变动,把相关内存页(card)标记为 dirty,为重标记阶段缩小范围
它本身不保证最终准确性,只是为后续修正打基础。
重新标记:聚焦脏卡区域,STW 时间可控
这是第二次 STW,但不是重扫全堆,而是专门处理并发标记期间的变动:
- 只检查卡表里标记为 dirty 的那些内存区域
- 重新扫描这些区域内的对象引用关系,修正漏标
- 如果年轻代较“脏”,可加参数 -XX:+CMSScavengeBeforeRemark,先触发一次 YGC,让重标记更轻量
虽然比初始标记慢,但通常控制在十几毫秒内,是 CMS 保证回收正确性的关键环节。


















