CMS收集器详解

一、CMS 核心定位:什么是 CMS 收集器?

1. 核心定义

CMS 是 HotSpot 虚拟机中一款老年代垃圾收集器 ,基于 "标记 - 清除" 算法实现,核心特点是并发收集、低停顿------ 垃圾收集线程与用户线程大部分时间可并行执行,大幅减少 STW(Stop The World)时间,提升应用响应速度。

2. 通俗比喻

把 JVM 老年代比作大型商场 ,用户线程是购物的顾客 ,GC 线程是清洁工人

  • 其他收集器(如 Serial Old):清洁时清场,顾客全部暂停(STW),清洁完再让顾客进入,停顿时间长;
  • CMS 收集器:清洁工人与顾客并行工作 ------ 先快速标记需要清洁的区域(短暂清场),然后一边让顾客购物,一边清理垃圾,仅在关键步骤短暂暂停顾客,停顿时间极短。

3. 适用场景与搭配

  • 适用场景:响应时间优先的服务(如 Web 网站、API 接口、微服务),要求 GC 停顿时间控制在百毫秒内;
  • 搭配收集器:CMS 仅负责老年代回收,新生代需搭配 ParNew 收集器(Serial 收集器的多线程版本),形成 "ParNew+CMS" 组合(JDK8 默认支持);
  • 不适用场景:吞吐量优先的场景(如计算密集型任务)、大内存场景(如堆内存≥16G,CMS 性能会下降)。

二、CMS 核心原理:"标记 - 清除"+"并发执行"

1. 核心算法:标记 - 清除

CMS 基于 "标记 - 清除" 算法实现,分为 "标记" 和 "清除" 两个核心阶段:

  • 标记阶段:识别老年代中存活的对象(通过可达性分析算法,以 GC Roots 为起点遍历);
  • 清除阶段:回收未被标记的垃圾对象,释放内存空间。

但传统 "标记 - 清除" 算法存在效率低、产生内存碎片的问题,CMS 通过 "并发执行" 和 "分阶段优化" 解决了效率问题,却保留了内存碎片的缺陷(后续会详细说明)。

2. 核心设计:并发执行

CMS 的 "低停顿" 核心源于 "并发"------ 标记和清除阶段的大部分工作与用户线程并行执行,仅在初始标记和重新标记两个关键步骤触发短暂 STW。

这里要明确 "并发" 与 "并行" 的区别:

  • 并发(Concurrent):GC 线程与用户线程同时运行(同一 CPU 核心交替执行);
  • 并行(Parallel):多个 GC 线程同时运行(利用多核 CPU 并行处理,如 ParNew 收集器)。CMS 是 "并发收集器"(与用户线程并发),同时也是 "并行收集器"(多个 GC 线程并行执行标记 / 清除)。

三、CMS 完整工作流程:4 个阶段 + STW 关键点

CMS 的垃圾回收过程分为 4 个阶段,其中仅 2 个阶段需要 STW,且停顿时间极短(通常在几十毫秒内),另外 2 个阶段与用户线程并发执行,几乎不影响应用运行。

1. 阶段 1:初始标记(Initial Mark)------ STW(短暂)

核心工作

仅标记GC Roots 直接关联的对象(如虚拟机栈中引用的对象、静态变量引用的对象),不遍历整个引用链。

通俗理解

清洁工人先快速标记商场中 "入口处直接可见的垃圾"(如门口的塑料袋),不深入商场内部检查,因此速度极快。

特点
  • STW 时间极短(通常 10ms 内),几乎无感知;
  • 单线程执行(早期版本),后续版本支持多线程并行,进一步缩短停顿。

2. 阶段 2:并发标记(Concurrent Mark)------ 并发(无 STW)

核心工作

从初始标记的对象出发,遍历整个老年代的引用链,标记所有存活的对象。

通俗理解

清洁工人在顾客购物的同时,从门口的垃圾出发,逐一检查商场内部的每个角落,标记所有需要清理的垃圾,顾客正常购物不受影响。

特点
  • 与用户线程并发执行,无 STW;
  • 耗时最长(占整个 GC 周期的 80% 以上),但不影响应用响应;
  • 可能产生 "浮动垃圾":并发标记期间,用户线程可能修改对象引用(如创建新对象、断开引用),导致部分垃圾未被标记(后续 GC 再清理)。

3. 阶段 3:重新标记(Remark)------ STW(短暂)

核心工作

修正并发标记期间因用户线程操作导致的标记偏差,重新标记被遗漏的存活对象。

通俗理解

清洁工人在顾客短暂暂停购物时,快速核对之前的标记结果,补充标记并发期间新增的存活对象(如顾客临时放下的物品),修正标记错误。

特点
  • STW 时间比初始标记稍长(通常几十毫秒),但仍远短于 Full GC;
  • 采用多线程并行执行,提升标记效率;
  • 核心优化:使用 "增量更新"(Incremental Update)机制,处理并发标记期间的引用变化(具体原理见下文 "关键优化")。

4. 阶段 4:并发清除(Concurrent Sweep)------ 并发(无 STW)

核心工作

遍历老年代内存,回收所有未被标记的垃圾对象,释放内存空间。

通俗理解

清洁工人在顾客恢复购物后,清理所有之前标记的垃圾,顾客正常购物不受影响。

特点
  • 与用户线程并发执行,无 STW;
  • 不移动存活对象,因此会产生内存碎片(后续会讲解决方案);
  • 回收过程中,用户线程可继续创建新对象,若老年代内存不足,会触发 "Concurrent Mode Failure"(并发模式失败),转而执行 Serial Old 收集器的 Full GC(停顿时间极长)。

总结:CMS 工作流程时序图

复制代码
[初始标记(STW,10ms)] → [并发标记(无STW,数百ms)] → [重新标记(STW,50ms)] → [并发清除(无STW,数百ms)]

整个 GC 周期中,STW 总时间通常控制在 100ms 内,远优于 Serial Old(秒级停顿)和 Parallel Old(百毫秒~秒级停顿)。

四、CMS 的关键优化:增量更新(Incremental Update)

在并发标记阶段,用户线程可能修改对象引用(如黑色对象新增对白色对象的引用、灰色对象断开对白色对象的引用),导致 "漏标"(白色对象本是存活的却被标记为垃圾)。

CMS 通过 "增量更新" 机制解决漏标问题,核心逻辑:

  1. 当黑色对象(已标记且引用链遍历完成)新增对白色对象(未标记)的引用时,通过 "写屏障" 记录该引用关系;
  2. 重新标记阶段,以这些黑色对象为根,再次遍历引用链,补充标记遗漏的白色对象;
  3. 确保所有存活对象都被正确标记,避免 "错杀" 对象。

五、CMS 的优缺点:低停顿的代价

1. 优点(核心竞争力)

(1)低停顿(核心)

STW 时间极短,适合响应时间优先的场景,能显著提升用户体验(如 Web 应用接口响应更快、无明显卡顿)。

(2)并发执行

标记和清除阶段与用户线程并发,不占用过多 CPU 资源,对吞吐量影响较小。

(3)多线程并行

初始标记、重新标记阶段支持多线程并行,充分利用多核 CPU 优势,缩短 STW 时间。

2. 缺点(无法回避的问题)

(1)产生内存碎片

基于 "标记 - 清除" 算法,清除垃圾后会产生大量不连续的内存碎片。后果:

  • 大对象无法分配连续内存,即使老年代总内存充足,也会触发 Full GC;
  • 频繁的 Full GC 会导致应用卡顿,抵消低停顿的优势。
(2)并发模式失败(Concurrent Mode Failure)

并发清除阶段,用户线程持续创建新对象,若老年代内存快速被占满,CMS 无法及时回收,会触发 "并发模式失败",JVM 会自动切换到 Serial Old 收集器执行 Full GC(单线程、标记 - 整理算法),STW 时间极长(秒级)。

(3)占用额外 CPU 资源

并发标记和清除阶段,GC 线程与用户线程竞争 CPU 资源,会导致应用吞吐量下降(通常下降 10%~20%)。在单核 CPU 环境下,CMS 性能会严重退化。

(4)产生浮动垃圾

并发标记阶段,用户线程创建的新对象或修改引用产生的垃圾,无法在本次 GC 中回收,需等到下次 GC 清理,称为 "浮动垃圾"。浮动垃圾会占用老年代内存,增加并发模式失败的风险。

(5)不支持压缩(整理)

CMS 仅做 "标记 - 清除",不移动存活对象,无法解决内存碎片问题(后续会讲优化方案)。

六、CMS 实战调优:参数配置与问题解决

1. 核心 JVM 参数(启用与基础配置)

(1)启用 CMS 收集器
复制代码
-XX:+UseConcMarkSweepGC  # 启用CMS收集器(老年代)
-XX:+UseParNewGC          # 新生代使用ParNew收集器(与CMS搭配)

启用后,JVM 默认新生代用 ParNew、老年代用 CMS,形成 "ParNew+CMS" 组合。

(2)内存相关参数
复制代码
-Xms4g -Xmx4g              # 堆内存初始/最大=4G
-Xmn2g                     # 新生代=2G(堆的50%),减少老年代压力
-XX:SurvivorRatio=8        # 新生代Eden:S0:S1=8:1:1(默认)
-XX:MaxTenuringThreshold=15 # 对象晋升老年代年龄阈值(默认15)
(3)CMS 核心调优参数
参数 作用 推荐配置
-XX:CMSInitiatingOccupancyFraction 老年代使用率阈值,达到该值触发 CMS GC(默认 92%) 80~85(提前触发,避免并发模式失败)
-XX:+UseCMSInitiatingOccupancyOnly 仅按上述阈值触发 CMS,不动态调整 启用(-XX:+UseCMSInitiatingOccupancyOnly)
-XX:ParallelCMSThreads CMS 并发线程数(默认 = CPU 核心数 + 3)/4 按 CPU 核心数调整(如 8 核设为 2~4)
-XX:+CMSFullGCsBeforeCompaction 执行 N 次 Full GC 后进行一次内存整理(解决碎片) 0~3(默认 0,即每次 Full GC 后整理)
-XX:+CMSCompactAtFullCollection Full GC 时进行内存整理(压缩) 启用(默认启用)
-XX:CMSMaxAbortablePrecleanTime 预清理阶段最大时间(默认 5 秒) 5000ms(根据业务调整)

2. 常见问题与解决方案

(1)并发模式失败(Concurrent Mode Failure)

现象 :日志中出现Concurrent Mode Failure,随后触发 Serial Old Full GC,停顿时间极长。

原因

  • 老年代使用率阈值设置过高(默认 92%),CMS 启动过晚,并发清除阶段内存不足;
  • 新生代对象晋升过快,老年代被快速占满;
  • 浮动垃圾过多,占用老年代内存。

解决方案

  • 降低-XX:CMSInitiatingOccupancyFraction至 80~85,让 CMS 提前触发;
  • 增大老年代内存(如-Xmx从 4G 调至 6G),或调整新生代比例(-Xmn),减少对象晋升;
  • 优化应用代码,减少大对象创建和长期存活对象。
(2)内存碎片过多

现象:老年代总内存充足,但频繁触发 Full GC,日志中显示 "老年代碎片率高"。

原因:CMS "标记 - 清除" 算法产生的内存碎片,导致大对象无法分配连续内存。

解决方案

  • 启用-XX:+CMSCompactAtFullCollection(默认启用),Full GC 时自动整理内存;
  • 通过-XX:CMSFullGCsBeforeCompaction=3,每执行 3 次 Full GC 后强制整理一次(平衡停顿时间和碎片率);
  • 避免创建过大的对象(如超过老年代内存的 10%),或通过-XX:PretenureSizeThreshold设置大对象阈值,让大对象直接进入老年代(减少晋升压力)。
(3)重新标记阶段 STW 时间过长

现象:重新标记阶段 STW 时间超过 100ms,影响应用响应。

原因

  • 并发标记期间用户线程修改的引用过多,重新标记需要遍历大量对象;
  • CMS 并发线程数不足,标记效率低。

解决方案

  • 增加-XX:ParallelCMSThreads线程数(如 8 核 CPU 设为 4),提升并行标记效率;
  • 启用-XX:+CMSScavengeBeforeRemark,重新标记前先执行一次 Minor GC,减少老年代引用的新生代对象,缩短标记时间;
  • 优化应用代码,减少并发标记期间的对象引用修改(如避免在高并发场景下频繁修改静态变量引用)。

3. CMS 日志分析示例

启用 CMS 后,GC 日志会包含如下关键信息(简化版):

复制代码
# 初始标记(STW)
[GC (CMS Initial Mark) [1 CMS-initial-mark: 15360K(30720K)] 17920K(40960K), 0.0050000 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

# 并发标记
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.2000000 secs] [Times: user=0.50 sys=0.10, real=0.20 secs]

# 重新标记(STW)
[GC (CMS Final Remark) [YG occupancy: 2560K (10240K)] [Rescan (parallel) , 0.0300000 secs] [Weak Ref Processing, 0.0010000 secs] [Class Unloading, 0.0020000 secs] [Scrub Symbol Table, 0.0030000 secs] [Scrub String Table, 0.0010000 secs] [1 CMS-remark: 15360K(30720K)] 17920K(40960K), 0.0370000 secs] [Times: user=0.10 sys=0.01, real=0.04 secs]

# 并发清除
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.1500000 secs] [Times: user=0.40 sys=0.08, real=0.15 secs]
  • CMS-initial-mark:初始标记,STW 时间 0.01 秒;
  • CMS-concurrent-mark:并发标记,耗时 0.2 秒(无 STW);
  • CMS Final Remark:重新标记,STW 时间 0.04 秒;
  • CMS-concurrent-sweep:并发清除,耗时 0.15 秒(无 STW);
  • 总 STW 时间仅 0.05 秒,符合低停顿目标。

七、CMS 与 G1 收集器的对比:为什么 CMS 会被淘汰?

JDK9 后 G1 取代 CMS 成为默认收集器,核心原因是 G1 解决了 CMS 的诸多缺陷,同时保留了低停顿优势。两者关键对比:

对比项 CMS 收集器 G1 收集器
核心算法 标记 - 清除(老年代) 标记 - 整理(局部复制,全局整理)
内存碎片 有(严重) 无(内存连续)
停顿可预测性 不可预测 可预测(通过-XX:MaxGCPauseMillis设置)
内存布局 新生代 + 老年代(物理隔离) Region 分区(逻辑分代,物理不隔离)
并发模式失败 易触发(切换 Serial Old) 不易触发(动态调整 Region)
大内存支持 差(堆≥16G 性能下降) 好(堆≥32G 仍高效)
吞吐量 中等(并发占用 CPU) 较高(优化的并行执行)

结论

  • 若使用 JDK9+,优先选择 G1 收集器,无需纠结 CMS;
  • 若维护 JDK8 及以前的老系统,且对响应时间要求高,可使用 CMS,但需做好调优(避免并发模式失败和内存碎片);
  • 计算密集型、吞吐量优先的场景,推荐使用 Parallel Scavenge+Parallel Old 组合,而非 CMS。
相关推荐
旺仔.2912 小时前
死锁 详解
linux·开发语言·计算机网络·安全
毅炼2 小时前
Spring总结(2)
java·数据库·sql·spring
xuhaoyu_cpp_java2 小时前
Servlet学习
java·笔记·学习
左左右右左右摇晃2 小时前
JVM 整理(二) 类加载器
jvm·笔记
I love studying!!!2 小时前
python项目: 下载数据
开发语言·python
不只会拍照的程序猿2 小时前
《嵌入式AI筑基笔记03:Python流程控制,从C的严谨到Python的简洁》
c语言·开发语言·笔记·python
阴暗扭曲实习生2 小时前
基于135编辑器的SaaS/PaaS服务集成实践
java·编辑器·paas
问今域中2 小时前
java技术史001:EJB 侵入性的历史阵痛与 Spring 的突围
java·开发语言·rpc
BUG创建者2 小时前
openlayers上跟据经纬度画出轨迹
开发语言·javascript·vue·html