CMS垃圾收集器

131 阅读8分钟

CMS垃圾收集器

一、CMS的内存划分

cms是线性的空间,每个对象线性的分布在内存中,同时cms采用了标记-清除算法,使cms可以和用户线程并发执行,而不再是完全的用户线程停止。

二、CMS 四阶段全流程与用户线程状态

1. 初始标记(Initial Mark)

阶段目标:标记所有直接被 GC Roots 引用的老年代对象(即“根对象”)。

用户线程状态必须停顿(STW)

关键操作

  • 扫描老年代中所有被 GC Roots(如栈帧局部变量、静态变量、JNI 引用等)直接引用的对象。
  • 由于需要确保扫描的准确性(避免扫描过程中对象引用被修改),必须暂停所有用户线程。

耗时:通常极短(1-10ms),是 CMS 中唯一必须 STW 的阶段。


2. 并发标记(Concurrent Mark)

阶段目标:追踪并标记所有从根对象出发可达的老年代对象(即“存活对象”),只扫描老年代的空间,不会扫码新生代引用老年代的对象。

用户线程状态无需停顿(与应用线程并行执行)。

关键操作

  • CMS 线程与应用线程同时运行,CMS 线程通过遍历对象引用链(如对象 A 引用对象 B,对象 B 引用对象 C),标记所有可达的老年代对象。
  • 允许用户线程继续执行新对象分配、引用更新等操作(但会通过“增量更新”机制记录引用变更)。

耗时:与应用运行时间重叠,通常较长(200-2000ms),但用户无感知。

并发扫码带来的问题:无法判断对象到底是存活还是死亡


3. 重新标记(Remark)

阶段目标:修正并发标记阶段因用户线程操作导致的标记错误(如新增对象被引用、原有引用被删除)。

用户线程状态必须停顿(STW)

关键操作

  • 扫描并发标记阶段记录的“增量更新”(如用户线程新创建的对象、删除的引用关系)。
  • 重新标记所有被遗漏的存活对象,确保标记结果的准确性。
  • 扫描新生代,处理新生代引用老年代的情况。

耗时:通常中等(50-500ms),是 CMS 中第二个需要 STW 的阶段。


4. 并发清除(Concurrent Sweep)

阶段目标:清理所有未被标记的老年代对象(即“垃圾对象”),释放内存空间。

用户线程状态无需停顿(与应用线程并行执行)。

关键操作

  • CMS 线程扫描老年代,将未被标记的对象内存标记为“可重用”。
  • 允许用户线程继续分配新对象(新对象会被分配到已清理的空闲区域)。

耗时:与应用运行时间重叠,通常较长(100-1000ms),但用户无感知。


二、用户线程停顿的本质原因

1. 必须停顿的阶段(初始标记、重新标记)

  • 原因:需要确保标记的准确性。
    • 初始标记阶段:若用户线程运行,可能新增或修改对象引用,导致扫描结果不完整。
    • 重新标记阶段:并发标记阶段用户线程可能修改了引用关系(如 A 不再引用 B),若不暂停用户线程,无法捕捉这些变更,导致标记错误(存活对象被误判为垃圾,或垃圾对象被误判为存活)。

2. 无需停顿的阶段(并发标记、并发清除)

  • 原因:CMS 采用“增量更新”机制记录用户线程的引用变更,允许与应用线程并行执行。
    • 并发标记阶段:用户线程的引用更新会被记录到“修改队列”,后续由重新标记阶段统一处理。
    • 并发清除阶段:用户线程的新对象分配会被分配到已清理的空闲区域,不影响标记结果。

三、典型场景下的用户线程状态总结

阶段用户线程状态停顿原因耗时占比
初始标记停顿(STW)确保根对象扫描的准确性5%-10%
并发标记运行并行追踪引用链40%-50%
重新标记停顿(STW)修正并发标记的引用变更10%-20%
并发清除运行并行清理垃圾对象30%-40%

四、常见误解澄清

1. “并发标记完全无开销”

错误:并发标记虽无需用户线程停顿,但 CMS 线程与应用线程会竞争 CPU 资源,可能导致应用线程延迟增加(尤其在 CPU 核心数较少时)。

2. “重新标记必须扫描整个老年代”

错误:重新标记仅扫描“增量更新”记录的变更部分(如用户线程新增的引用),而非全量扫描老年代,因此耗时远低于初始标记。

3. “并发清除会导致内存碎片”

正确:CMS 使用“标记-清除”算法,清除后内存空间可能不连续,长期运行可能导致内存碎片(需通过 -XX:+UseCMSCompactAtFullCollection 触发 Full GC 整理)。


五、如何解决新生代引用老年代的情况

CMS并未很好的解决这个问题,目前采用和serial一样的处理方式,在重新标记的阶段,停止用户线程,通过完整的扫描新生代的对象,来找到跨代引用。


六、CMS如何存储晋升的对象

cms之前的收集器采用标记整理,所以空间连续,可以使用指针碰撞来给晋升的对象分配空间,但是cms采用标记清除,所以只有使用空闲链表来管理空闲的空间

1.cms分配空间的流程

graph TD
    A[对象晋升请求] --> B{检查空闲链表}
    B -->|找到合适块| C[分配成功]
    B -->|无合适块| D{碎片整理策略}
    D -->|开启压缩| E[触发Full GC整理内存]
    D -->|未开启压缩| F[尝试分配小对象填充]
    F -->|碎片可容纳小对象| G[碎片被利用]
    F -->|碎片过小| H[碎片被浪费]

2.碎片处理决策树

image.png

3.小对象填充(CMS默认策略)

sequenceDiagram
    CMS->>FreeList: 晋升对象需50KB
    FreeList->>搜索: 最大块40KB(不足)
    FreeList->>检查: 发现10KB碎片
    CMS->>小对象: 尝试分配5KB对象
    FreeList->>碎片: 切割5KB分配
    Note right of FreeList: 剩余5KB仍为碎片

4.碎片合并策略(Binning)(默认未启用)

image.png

实现方式

  • 维护多个尺寸的空闲链表(8KB/16KB/32KB...)
  • 定期合并相邻碎片
  • 默认关闭,CMS需开启 XX:+UseCMSCompactAtFullCollection

5.定期执行Full GC解决碎片化

graph TD
    A[CMS并发收集周期] --> B{计数检查}
    B -->|达到阈值| C[触发Full GC整理]
    B -->|未达阈值| D[继续并发收集]
  • 参数名XX:CMSFullGCsBeforeCompaction=N
  • 作用:指定在执行 N 次 CMS 并发收集周期(Old GC)后,触发 1 次带压缩的 Full GC
  • 默认值:0(每次并发收集失败后都进行压缩)
配置值效果适用场景
N=0每次并发收集失败后都压缩内存敏感型系统
N=1每1次Old GC后都压缩碎片产生快的系统
N=5每5次Old GC后压缩1次平衡型配置(推荐)
N=1000几乎不压缩延迟敏感型系统

七.CMS空闲链表详解

CMS的内存管理机制并非仅依赖单一的空闲链表结构,而是采用了多层复合数据结构,主要包括以下三类管理方式,共同协作以提升老年代内存分配的效率:

📌 1. 基础空闲链表(Free List)

  • 结构描述:CMS使用一条主空闲链表记录所有空闲内存块(即未被使用的内存区域)。每个空闲块通过链表指针串联,形成链式结构。
  • 操作逻辑:分配内存时需遍历链表,寻找大小匹配的空闲块;若找到则拆分该块(剩余部分仍放回链表),否则触发GC或内存压缩。
  • 作用:兜底分配,管理所有空闲块
  • 场景:当分类链表/树索引无法分配时使用。
  • 问题:全链表遍历效率低,尤其在高碎片场景下耗时显著。
  • 初始化: 初始启动时,基础链表就一个覆盖老年代索引空间的节点,然后申请空间后剩余按照下面介绍的剩余空间处理流程。

🧩 2. 按大小分类的多链表(Segregated Free Lists)

  • 优化设计:将空闲块按固定大小区间分组(如0-512B、512B-1KB等),每组对应一个独立链表。分配时直接定位目标大小区间链表,减少遍历范围。
  • 性能提升:小对象分配效率显著提高(如分配128B对象时直接访问“小对象链表”)。
  • 局限性:无法覆盖所有对象大小(例如10KB对象需扫描多个链表)

🌳 3. 树形索引结构(Tree-structured Index)

  • 高级优化:为多链表建立二叉树索引。树节点指向不同大小区间的链表头,通过树搜索快速定位目标链表。
  • 优势
    • 对数级时间复杂度(O(log n)),加速大块内存分配。
    • 支持动态合并相邻空闲块,减少碎片(如释放两个相邻块时自动合并为更大块)。
  • 代价:树结构维护增加内存开销(需额外指针)。
  • 节点: 树里的节点是分类链表的头

4.使用流程:

graph TD
    A[分配请求] --> B{对象大小}
    B -->|小对象| C[分类多链表]
    B -->|中/大对象| D[树形索引]
    
    C --> E[直接访问对应链表]
    D --> F[树搜索定位链表]
    
    E --> G{空间足够?}
    F --> G
    
    G -->|是| H[分配碎片]
    G -->|否| I[回退基础链表]
    I --> J[遍历基础链表]
    J --> K{找到空间?}
    K -->|是| H
    K -->|否| L[触发GC]

5.空间剩余继续加入空闲链表流程:

注意:如果剩余空间小于 -XX:CMSMinFreeSpace 的默认值(128B),cms会将碎片丢弃,直到Full GC后,才会重新使用。

graph TD
    A[分配后剩余空间] --> B[创建新碎片头]
    B --> C[加入基础链表]
    C --> D{大小匹配分类链表?}
    D -->|是| E[加入对应分类链表]
    D -->|否| F[仅基础链表]
    
    E --> G[分类链表更新]
    G --> H[树索引不变]
    
    F --> I[基础链表维护]

总结:CMS 四阶段用户线程停顿规律

  • 必须停顿:初始标记(标记根对象)、重新标记(修正引用变更)。
  • 无需停顿:并发标记(并行追踪引用链)、并发清除(并行清理垃圾)。
  • 核心设计:通过“增量更新”机制平衡并发性能与标记准确性,是传统分代收集器中低延迟回收的典型代表(但存在内存碎片问题,现代应用建议优先考虑 G1/ZGC)。