当前位置: 首页 > news >正文

JVM(11)——详解CMS垃圾回收器

CMS (Concurrent Mark-Sweep) 垃圾回收器。它是 JDK 1.4 后期引入,并在 JDK 5 - JDK 8 期间广泛使用的一种以低停顿时间 (Low Pause Time) 为主要目标的老年代垃圾回收器。它是 G1 出现之前解决 Full GC 长停顿问题的主要方案。

一、CMS 的设计目标与定位

  1. 核心目标:最小化应用停顿时间 (STW - Stop-The-World)。

    • 特别关注老年代垃圾回收引起的停顿。

    • 旨在避免传统 Serial Old GC(标记-整理)导致的长时间、全局性的停顿,这对交互式应用(如 Web 服务器、GUI 应用)至关重要。

  2. 实现方式:并发 (Concurrent) 收集。

    • 将垃圾回收中最耗时的标记 (Marking) 和清除 (Sweeping) 阶段,尽可能与应用线程并发执行,从而大大减少需要 STW 的时间。

  3. 算法:标记-清除 (Mark-Sweep)。

    • 标记: 找出所有存活的对象。

    • 清除: 回收未被标记(即死亡)的对象占用的空间。

    • 注意: 它不进行压缩 (Compaction),这是它产生内存碎片的主要原因。

  4. 分代收集: CMS 主要管理老年代 (Old Generation)。它通常与一个年轻代收集器搭配使用,如 ParNew(并行复制算法,Serial 的多线程版本)或 Serial

  5. 定位: 在 G1 成熟之前,CMS 是追求低延迟老年代回收的首选方案,尤其适用于中小型堆(如 4GB - 8GB)、CPU 资源相对充足且能容忍一些内存碎片和额外 CPU 开销的应用。

二、CMS 的工作阶段 (Phases)

CMS 的垃圾回收周期(针对老年代)由多个阶段组成,其中只有部分阶段需要 STW:

  1. 初始标记 (Initial Mark - STW)

    • 目标: 标记从 GC Roots 直接可达 的老年代对象(速度很快)。

    • STW 原因: 需要暂停应用线程以确保在一致性的快照下快速扫描 GC Roots(线程栈、静态变量、JNI 引用等)。

    • 优化: 这个阶段通常借道一次 Young GC(Minor GC)来完成。因为 Young GC 本身就需要 STW 并扫描 GC Roots,CMS 可以“搭便车”标记那些从根集直接引用的老年代对象,避免了额外的完整根扫描停顿。所以 CMS 触发的时机往往紧跟在一次 Young GC 之后。

  2. 并发标记 (Concurrent Mark - Concurrent)

    • 目标: 从“初始标记”阶段标记的直接可达对象开始,遍历整个老年代对象图,标记所有间接可达的存活对象。

    • 并发性: 这是 CMS 减少停顿的关键!这个阶段与应用线程同时运行。应用线程可以继续创建新对象、更新引用关系。

    • 挑战 - 浮动垃圾 (Floating Garbage): 因为在标记过程中应用线程还在运行,可能会产生新的垃圾对象(标记阶段结束后才成为垃圾)或者使已标记的对象变成垃圾。同时,应用线程修改对象引用关系可能导致“漏标”或“多标”问题。CMS 使用 增量更新 (Incremental Update) 算法来解决对象引用变化的问题(与 G1 的 SATB 不同)。

    • 挑战 - 耗时: 遍历整个老年代对象图,即使并发,也可能花费较长时间,特别是大堆。

  3. 重新标记 (Remark - STW)

    • 目标: 修正“并发标记”阶段因应用线程继续运行而导致的标记变动,确保所有在并发标记期间存活的对象都被正确标记。处理在并发阶段新晋升到老年代的对象(如果搭配 ParNew,晋升发生在 Young GC 的 STW 阶段,相对容易处理)。

    • STW 原因: 为了获得一个最终准确的存活对象视图,需要在一个确定的点上暂停所有应用线程。

    • 优化: 这个阶段通常比“初始标记”长,但比“并发标记”短得多。JVM 会使用多线程并行处理来加速。可以启用 -XX:+CMSScavengeBeforeRemark 参数,在重新标记前强制触发一次 Young GC,清理掉年轻代的垃圾,减少需要扫描的年轻代对象数量(年轻代对象也可能引用老年代对象),从而缩短 STW 时间。

  4. 并发清除 (Concurrent Sweep - Concurrent)

    • 目标: 回收那些在标记阶段被确定为死亡对象所占用的内存空间。

    • 并发性: 这个阶段也与应用线程同时运行。应用线程可以继续分配新对象(在空闲列表管理的内存区域)。

    • 算法: 使用空闲列表 (Free List) 管理回收后的空间。清除器遍历内存,将连续的死对象空间合并成空闲块,记录在空闲列表中,供后续分配使用。

    • 结果: 回收了垃圾内存,但不进行内存整理压缩。这导致了内存碎片问题。

  5. 并发重置 (Concurrent Reset - Concurrent)

    • 目标: 为下一次 CMS 周期重置内部数据结构(如标记位图)。

    • 并发性: 与应用线程同时运行,无停顿。

三、CMS 的核心特性与优势

  1. 低停顿时间 (Low Pause Time): 这是 CMS 最大的优势。通过将最耗时的标记和清除工作并发执行,显著减少了 STW 的时间(主要集中在初始标记和重新标记阶段),使得老年代回收对应用响应时间的影响大大降低。

  2. 并发收集 (Concurrent Collection): 真正实现了垃圾回收线程与应用线程在大部分时间并行工作。

  3. 适用于延迟敏感型应用: 在 G1 成熟之前,是 Web 服务器、交易系统等需要快速响应的应用的首选老年代回收器。

四、CMS 的缺点与挑战

  1. 内存碎片 (Memory Fragmentation):

    • 根本原因: 使用标记-清除算法且不压缩内存。长时间运行后,老年代会由许多存活对象和大小不一、分散的空闲内存块组成。

    • 后果:

      • 分配失败: 即使老年代总的空闲空间足够,也可能因为找不到足够大的连续空间来分配一个大对象(或晋升对象),从而触发 Full GC (Serial Old GC)

      • Full GC 时间长: Serial Old GC 是单线程的标记-整理-压缩算法,在大堆上进行压缩会导致非常长的 STW 停顿,违背了使用 CMS 的初衷。

    • 缓解措施:

      • -XX:+UseCMSCompactAtFullCollection (默认 true): 在不得不进行 Full GC 时,在 Full GC 后进行内存压缩。

      • -XX:CMSFullGCsBeforeCompaction=n (默认 0): 设定在多少次不压缩的 Full GC 后,执行一次带压缩的 Full GC。0 表示每次 Full GC 都压缩(推荐)。但这仍然意味着要经历一次长时间的 Full GC。

  2. 并发模式失败 (Concurrent Mode Failure):

    • 触发条件:

      • 老年代空间不足: 在 CMS 并发周期(标记和清除)完成之前,老年代空间就被填满了。这通常发生在:

        • 老年代分配/晋升速率过快,超过 CMS 回收速度。

        • 浮动垃圾过多,占用了本应回收的空间。

        • 并发周期启动太晚(-XX:CMSInitiatingOccupancyFraction 设置过高)。

      • 晋升失败 (Promotion Failed): Young GC 后,存活对象需要晋升到老年代,但老年代没有足够的连续空间容纳它们(即使总空间可能够,但碎片导致)。

    • 后果: JVM 会立即中断 CMS 并发周期,并触发一次 Full GC (Serial Old GC)。这会导致一个计划外的、长时间的 STW 停顿。

    • 预防措施:

      • 合理设置 -XX:CMSInitiatingOccupancyFraction降低老年代空间占用阈值(如从默认 68% 设到 50% 或更低),尽早启动 CMS 并发周期,给并发回收留出足够的时间窗口和空间裕度。

      • 必须配合 -XX:+UseCMSInitiatingOccupancyOnly 使用:确保 JVM 根据 CMSInitiatingOccupancyFraction 的值启动 CMS,而不是自行“自适应”调整(可能导致启动过晚)。

      • 增加堆大小或老年代比例 (-Xmx-Xms-XX:NewRatio)。

      • 优化应用,减少对象创建和晋升速率、减小对象大小、避免过大的对象。

      • 增加 CMS 回收线程数 (-XX:ConcGCThreads / -XX:ParallelCMSThreads, 后者在较新版本已废弃,推荐用 ConcGCThreads)。

  3. 对 CPU 资源敏感 (CPU Sensitive):

    • 原因: 并发标记和并发清除阶段需要与应用线程争抢 CPU 资源。

    • 后果:

      • 在 CPU 资源紧张(如 CPU 核数少、负载高)的情况下,并发回收线程会拖慢应用线程的执行速度,导致应用吞吐量下降

      • 并发阶段本身可能因为 CPU 争抢而执行得更慢,增加了并发模式失败的风险。

    • 建议: CMS 更适合 CPU 资源相对富余(核数较多或负载不高)的机器。

  4. 浮动垃圾 (Floating Garbage): 如前所述,并发过程中产生的垃圾只能在下一次 GC 回收。需要预留足够空间容纳这些浮动垃圾。

  5. 元空间/永久代触发 Full GC: CMS 不管理元空间 (Metaspace, JDK 8+) 或永久代 (PermGen, JDK 7-)。如果元空间/永久代空间不足,会触发 Full GC。

  6. JDK 9+ 中已弃用 (Deprecated),JDK 14+ 中已移除 (Removed):

    • 弃用原因: G1 作为更现代、设计更优的回收器(同样追求低延迟,且解决了碎片问题)已成为默认选择。CMS 的维护成本高,且其架构难以适应更新的 Java 特性和硬件发展(如非常大的堆)。

    • 后果: 在新版本 JDK (>=14) 中无法再使用 CMS。仍在使用的应用应尽快迁移到 G1 或其他回收器(如 ZGC, Shenandoah)。

五、关键配置参数

  • 启用 CMS:

    • -XX:+UseConcMarkSweepGC (JDK 8 及之前)

  • 设置年轻代收集器 (通常自动选择):

    • 搭配 ParNew:-XX:+UseParNewGC (通常启用 CMS 会自动启用)

  • 触发 CMS 的堆占用阈值 (最重要!):

    • -XX:CMSInitiatingOccupancyFraction=<percent> (e.g., 70): 当老年代空间占用达到此百分比时,启动 CMS 并发收集周期。建议设低一些(如 50-70)以预防并发模式失败。

  • 强制使用阈值触发 (必须配!):

    • -XX:+UseCMSInitiatingOccupancyOnly强制 JVM 只使用 CMSInitiatingOccupancyFraction 的值作为触发条件,禁用 JVM 的自适应调整。

  • 重新标记前进行 Young GC:

    • -XX:+CMSScavengeBeforeRemark: 在重新标记阶段前强制触发一次 Young GC,减少需要扫描的年轻代对象,有效缩短重新标记 STW 时间(强烈推荐启用)。

  • CMS 线程数:

    • -XX:ConcGCThreads=<n> / -XX:ParallelCMSThreads=<n> (后者较旧): 设置并发阶段(标记、清除)使用的线程数。默认为 (ParallelGCThreads + 3) / 4。可根据 CPU 核数调整。

  • Full GC 后压缩:

    • -XX:+UseCMSCompactAtFullCollection (默认 true): Full GC 后进行压缩。

    • -XX:CMSFullGCsBeforeCompaction=<n> (默认 0): 执行 n 次不压缩的 Full GC 后,执行一次带压缩的 Full GC。0 表示每次都压缩。

六、何时使用(或曾经使用)CMS?

  • 历史场景 (JDK 8 及之前):

    • 应用对老年代回收停顿时间非常敏感

    • 应用运行在中小型堆(如 4GB - 8GB)上。

    • 机器有富余的 CPU 资源(核数较多,负载不高)。

    • 应用能够容忍一定程度的内存碎片或通过配置降低了 Full GC 风险。

    • 需要避免 Serial Old GC 的长停顿

  • 当前状态:

    • JDK 9+:已弃用 (Deprecated)。

    • JDK 14+:已移除 (Removed)。

    • 强烈建议所有仍在使用 CMS 的应用迁移到 G1(目前默认且成熟)或探索新一代超低延迟回收器 ZGC / Shenandoah(尤其超大堆和极致低延迟需求)。

七、总结

CMS 垃圾回收器是 JVM 垃圾回收发展史上一个重要的里程碑,它率先通过并发标记清除的方式显著降低了老年代回收的停顿时间,满足了当时众多对延迟敏感型 Java 应用的需求。其核心价值在于并发性带来的低 STW 停顿

然而,CMS 的固有缺陷也非常明显:

  1. 标记-清除算法导致内存碎片, 最终可能引发长时间的 Serial Old Full GC。

  2. 对并发模式失败 (Concurrent Mode Failure) 非常敏感, 需要精细调优(尤其是 CMSInitiatingOccupancyFraction 和 UseCMSInitiatingOccupancyOnly)。

  3. 并发阶段占用 CPU 资源,影响吞吐量。

  4. 无法管理元空间/永久代。

  5. 已被现代 JDK (>=14) 彻底移除。

http://www.lryc.cn/news/574853.html

相关文章:

  • 猿人学js逆向比赛第一届第十二题
  • CDN+OSS边缘加速实践:动态压缩+智能路由降低30%视频流量成本(含带宽峰值监控与告警配置)
  • RSS解析并转换为JSON的API集成指南
  • SQL Server从入门到项目实践(超值版)读书笔记 18
  • [学习] C语言编程中线程安全的实现方法(示例)
  • 【Datawhale组队学习202506】YOLO-Master task04 YOLO典型网络模块
  • Python训练营-Day40-训练和测试的规范写法
  • 【Python-Day 29】万物皆对象:详解 Python 类的定义、实例化与 `__init__` 方法
  • 【Linux网络与网络编程】15.DNS与ICMP协议
  • 性能测试-jmeter实战4
  • 集成学习基础:Bagging 原理与应用
  • PyEcharts教程(009):PyEcharts绘制水球图
  • 60天python训练营打卡day41
  • Linux系统---Nginx配置nginx状态统计
  • 鸿蒙 Stack 组件深度解析:层叠布局的核心应用与实战技巧
  • AI时代工具:AIGC导航——AI工具集合
  • 接口自动化测试之pytest 运行方式及前置后置封装
  • 爬取小红书相关数据导入到excel
  • 项目需求评审报告参考模板
  • 图的拓扑排序管理 Go 服务启动时的组件初始化顺序
  • 飞往大厂梦之算法提升-day08
  • sqlserver怎样动态执行存储过程,并且返回报错
  • Java实现简易即时通讯系统
  • day41 打卡
  • 基于元学习的回归预测模型如何设计?
  • MySQL:深入总结锁机制
  • linux操作系统的软件架构分析
  • 战略调整频繁,如何快速重构项目组合
  • 原生策略与功耗方案参考
  • Android 开发问题:Wrong argument type for formatting argument ‘#2‘ in info_message