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.碎片处理决策树
3.小对象填充(CMS默认策略)
sequenceDiagram
CMS->>FreeList: 晋升对象需50KB
FreeList->>搜索: 最大块40KB(不足)
FreeList->>检查: 发现10KB碎片
CMS->>小对象: 尝试分配5KB对象
FreeList->>碎片: 切割5KB分配
Note right of FreeList: 剩余5KB仍为碎片
4.碎片合并策略(Binning)(默认未启用)
实现方式:
- 维护多个尺寸的空闲链表(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)。