可以把CMS 和 G1理解成两代不同思想的垃圾回收器:
CMS 的核心目标是:尽量缩短单次 STW 时间。
G1 的核心目标是:在可控停顿时间内,尽量回收更多垃圾。
一、CMS 垃圾回收器
CMS 全称是Concurrent Mark Sweep,中文一般叫并发标记清除垃圾回收器。
它主要用于老年代回收。
它的设计目标是:
响应时间优先,尽量减少 STW 时间。
也就是说,CMS 更关心的是:
用户线程不要停太久。
比如一些 Web 系统、接口服务,如果一次 GC 停顿几秒,用户请求就会明显卡顿,所以 CMS 希望把 GC 的大部分工作和用户线程并发执行。
二、CMS 的回收过程
CMS 的完整过程主要有四步:
初始标记 -> 并发标记 -> 重新标记 -> 并发清理1. 初始标记:STW
初始标记会暂停用户线程。
它只标记一小部分对象:
GC Roots 能直接关联到的对象。
比如:
线程栈中的引用 静态变量引用 常量引用 JNI 引用这些根对象直接能找到的对象,会被先标记出来。
因为这个阶段只标记直接关联对象,所以速度很快,STW 时间较短。
2. 并发标记:不需要 STW
并发标记阶段,GC 线程和用户线程同时执行。
它会从初始标记阶段找到的对象继续往下遍历,找到所有可达对象。
比如:
GC Roots ↓ 对象 A ↓ 对象 B ↓ 对象 C如果 A、B、C 都能从 GC Roots 间接访问到,那它们都不是垃圾。
这个阶段耗时较长,但因为它和用户线程并发执行,所以用户程序不会完全停下来。
这就是 CMS 停顿时间短的重要原因之一。
3. 重新标记:STW
问题来了。
并发标记的时候,用户线程还在运行。
用户线程可能会修改对象之间的引用关系。
比如原来是:
A -> B并发标记过程中,用户线程改成了:
A -> C这时候 GC 线程之前看到的对象关系可能已经过时了。
所以 CMS 需要进入重新标记阶段,再次暂停用户线程,修正并发标记期间发生变化的引用关系。
你写的“漏标”这个理解是对的,但可以说得更准确一点:
重新标记主要是为了修正并发标记期间,由于用户线程继续运行导致的标记变化,避免把仍然存活的对象误判为垃圾。
注意:GC 最怕的是把活对象当垃圾回收掉,这会导致程序错误。
4. 并发清理:不需要 STW
标记完成后,哪些对象是活的,哪些对象是垃圾,就已经知道了。
然后 CMS 会清理那些没有被标记的对象。
这个阶段也是并发执行的,GC 线程和用户线程同时工作。
但是 CMS 使用的是标记-清除算法。
所以它只是把垃圾对象占用的空间释放掉,并不会移动存活对象。
三、CMS 为什么 STW 时间短?
主要有两个原因。
第一,CMS 把耗时最长的两个阶段做成了并发:
并发标记 并发清理这两个阶段不需要长时间暂停用户线程。
第二,CMS 使用的是标记-清除算法。
标记-清除不需要移动对象,只需要把垃圾对象清掉,所以清理速度相对较快。
所以 CMS 的整体特点是:
停顿时间短 响应速度好 适合低延迟系统四、CMS 的缺点
CMS 的缺点也很明显,主要有三个。
1. 内存碎片问题
CMS 使用的是标记-清除算法。
标记-清除不会整理内存。
比如老年代原来是这样的:
[对象][垃圾][对象][垃圾][对象][垃圾]清理之后变成:
[对象][空闲][对象][空闲][对象][空闲]虽然空闲空间很多,但这些空间是不连续的。
这就会产生内存碎片。
假设现在有很多小空闲块:
2MB + 3MB + 4MB + 5MB总共 14MB 空间。
但是如果你要分配一个 10MB 的大对象,就可能失败,因为没有一块连续的 10MB 空间。
于是就可能触发 Full GC。
2. 浮动垃圾问题
CMS 在并发清理阶段,用户线程还在运行。
用户线程运行过程中,还可能继续产生新的垃圾。
这些垃圾是在 CMS 标记完成之后才产生的,所以本轮 CMS 没有办法处理它们。
这些垃圾就叫:
浮动垃圾。
比如:
CMS 已经完成标记 用户线程继续运行 用户线程又产生了一批垃圾这批垃圾只能等下一次 GC 再回收。
如果浮动垃圾太多,而老年代空间又不足,就可能出现:
Concurrent Mode Failure,并发模式失败。
这时候 CMS 就撑不住了,需要退化为 Full GC。
3. 并发执行会抢 CPU
CMS 的并发标记和并发清理虽然不会完全暂停用户线程,但 GC 线程和用户线程同时运行,会抢 CPU 资源。
如果服务器 CPU 核数比较少,CMS 可能会影响用户线程的执行效率。
所以 CMS 不是完全没有代价的。
五、CMS 退化为 Serial Old 的问题
当 CMS 出现问题,比如:
老年代空间不足 浮动垃圾太多 内存碎片严重 大对象分配失败就可能触发 Full GC。
CMS 的 Full GC 通常会退化为Serial Old。
Serial Old 是单线程老年代垃圾回收器。
它会:
STW 单线程回收 使用标记-整理算法标记-整理算法会移动对象,整理内存碎片。
比如:
[对象][空闲][对象][空闲][对象]整理后变成:
[对象][对象][对象][空闲][空闲]这样可以解决内存碎片问题。
但是代价是:
STW 时间非常长。
所以 CMS 的最大风险就是:
平时停顿很短,但一旦 Full GC,停顿可能非常严重。
六、G1 垃圾回收器
G1 全称是Garbage First。
意思是:
优先回收垃圾最多的区域。
G1 的设计目标是:
低延迟 高吞吐 可预测停顿时间 适合大堆内存和 CMS 不同,G1 不只是老年代回收器,它是一个面向整个堆的垃圾回收器。
七、G1 的堆内存结构
传统垃圾回收器一般把堆分成:
新生代 老年代比如:
Eden + Survivor + Old而 G1 把整个堆切成很多个大小相等的小块。
这些小块叫:
Region。
比如整个堆被切成这样:
Region 1 Region 2 Region 3 Region 4 Region 5 ...每个 Region 可以动态扮演不同角色:
Eden Region Survivor Region Old Region Humongous RegionHumongous Region 用来存放大对象。
所以 G1 不是完全不要分代,而是:
逻辑上仍然有新生代、老年代,但物理上不再是连续的大块空间,而是由一个个 Region 组成。
这一点很重要。
你写的“分区取代分代”可以稍微修正成:
G1 不是取消分代,而是用 Region 作为基本管理单位,让新生代和老年代都由一组不连续的 Region 组成。
八、G1 为什么可以控制 STW 时间?
G1 有一个很重要的参数:
-XX:MaxGCPauseMillis比如设置为:
200ms意思是希望 GC 停顿时间尽量控制在 200ms 左右。
注意,是“尽量”,不是绝对保证。
G1 的做法是:
不一定每次都回收整个堆,而是选择部分收益最高的 Region 回收。
比如现在有这些 Region:
Region A:垃圾 80% Region B:垃圾 70% Region C:垃圾 20% Region D:垃圾 10%如果暂停时间预算有限,G1 会优先选择:
Region A Region B因为它们垃圾最多,回收收益最高。
这就是 Garbage First 的含义:
垃圾最多的 Region 优先回收。
所以 G1 的思路是:
有限的停顿时间内 选择最值得回收的 Region 尽量回收更多空间九、G1 的回收过程
G1 的回收过程可以分成两类:
Young GC Mixed GC你写的那套过程更接近 G1 的并发标记周期和混合回收过程。
整体可以这样理解:
初始标记 -> 并发标记 -> 最终标记 -> 筛选回收/混合回收1. 初始标记:STW
初始标记也是标记 GC Roots 直接关联的对象。
这个阶段需要 STW,但时间很短。
在 G1 中,初始标记通常会借助一次 Young GC 一起完成。
2. 并发标记:不需要 STW
并发标记阶段,GC 线程和用户线程一起执行。
它会扫描整个堆,找出存活对象,并统计每个 Region 的垃圾比例。
比如:
Region A:存活对象 20%,垃圾 80% Region B:存活对象 30%,垃圾 70% Region C:存活对象 90%,垃圾 10%这些统计信息后面会用于制定回收计划。
3. 最终标记:STW
并发标记过程中,用户线程还在运行,对象引用关系可能发生变化。
所以 G1 也需要一个最终标记阶段,修正并发标记期间的变化。
这个阶段需要 STW。
4. 筛选回收 / 混合回收:STW
并发标记结束后,G1 知道了哪些 Region 垃圾比较多。
然后它会根据停顿时间目标,选择一部分 Region 进行回收。
这一步叫Mixed GC,混合回收。
为什么叫混合回收?
因为它回收的不只是新生代 Region,还可能包含部分老年代 Region。
比如:
Eden Region Survivor Region Old Region Humongous RegionG1 会选择其中一部分 Region 进行回收。
十、G1 使用什么垃圾回收算法?
G1 整体可以理解为:
局部复制算法 整体标记-整理思想在具体回收某些 Region 时,G1 会把这些 Region 中还活着的对象复制到新的空 Region 中。
然后直接清空原来的 Region。
比如:
回收前: Region A: [活对象][垃圾][活对象][垃圾] 复制存活对象到 Region B: Region B: [活对象][活对象] 然后清空 Region A。这样做的好处是:
不会产生 CMS 那种严重内存碎片因为存活对象被复制到了新的连续空间,原 Region 可以整体释放。
十一、G1 相比 CMS 的优势
1. 解决内存碎片问题
CMS 用标记-清除,不移动对象,所以容易产生碎片。
G1 回收 Region 时,会复制存活对象,然后整体清空旧 Region。
所以 G1 可以减少内存碎片。
2. 可预测停顿时间
CMS 的目标是减少停顿,但不太好控制每次停顿时间。
G1 可以根据暂停时间目标选择回收哪些 Region。
所以 G1 更强调:
可预测的停顿时间。
3. 更适合大堆
CMS 在大堆场景下容易出现碎片和 Full GC 问题。
G1 把堆划分成 Region,可以按区域回收,所以更适合大堆内存场景。
十二、G1 的缺点
G1 也不是完美的。
它的缺点主要是:
实现复杂 维护 Region 信息成本高 需要 Remembered Set 记录跨 Region 引用 小堆场景下不一定比传统回收器更快因为 G1 是按 Region 回收的,会遇到一个问题:
如果只回收某几个 Region,那怎么知道其他 Region 有没有引用这里面的对象?
比如:
Region A 里的对象 -> Region B 里的对象如果现在只回收 Region B,那必须知道 Region A 有没有引用 Region B。
所以 G1 需要维护额外的数据结构,叫:
Remembered Set,记忆集。
它用来记录跨 Region 的引用关系。
这会带来额外的内存和性能开销。
十三、CMS 和 G1 对比
| 对比项 | CMS | G1 |
|---|---|---|
| 设计目标 | 低停顿、响应时间优先 | 可预测停顿、兼顾吞吐 |
| 主要作用 | 老年代回收 | 整个堆回收 |
| 内存结构 | 新生代 + 老年代连续划分 | Region 分区管理 |
| 回收算法 | 标记-清除 | 复制 + 标记整理思想 |
| 是否容易产生碎片 | 容易 | 不容易 |
| 是否支持暂停时间目标 | 不强 | 支持 |
| Full GC 风险 | 较高 | 相对较低 |
| 适用场景 | 低延迟系统,老版本 JVM 常用 | 大堆、低延迟、可预测停顿系统 |
十四、面试时可以这样总结
你可以这样说:
CMS 是一款以响应时间优先为目标的老年代垃圾回收器,它采用标记-清除算法,主要流程包括初始标记、并发标记、重新标记和并发清理。其中并发标记和并发清理可以和用户线程同时执行,因此减少了 STW 时间。但是 CMS 存在内存碎片和浮动垃圾问题,当老年代空间不足或发生并发模式失败时,会退化为 Serial Old,触发长时间 Full GC。
然后说 G1:
G1 是一款面向整个堆的垃圾回收器,它把堆划分为多个大小相等的 Region,每个 Region 可以动态作为 Eden、Survivor、Old 或 Humongous 区。G1 可以根据用户设置的暂停时间目标,优先回收垃圾比例最高、收益最大的 Region。它的回收过程包括初始标记、并发标记、最终标记和混合回收。G1 通过复制存活对象到新的 Region,再整体清空旧 Region,减少了内存碎片问题,适合大堆内存、低延迟、可预测停顿时间的场景。
十五、你这份笔记需要改的几个点
你整体理解是对的,但有几个地方建议修正:
1. “对象消失”这个说法不太准确
你写:
可能会有些对象的引用关系变化,导致漏标(对象消失)
建议改成:
并发标记期间用户线程仍在运行,可能修改对象引用关系,导致标记结果不准确,因此需要重新标记来修正。
核心不是“对象消失”,而是引用关系变化导致标记结果需要修正。
2. “G1 不再分新生代和老年代”不够准确
建议改成:
G1 仍然保留分代思想,但物理上不再要求新生代和老年代是连续空间,而是由多个 Region 动态组成。
也就是说:
不是没有新生代和老年代 而是新生代和老年代不再是连续的大块内存3. G1 的混合回收不是一次回收所有区域
你写得基本对,但要强调:
Mixed GC 不是把所有老年代都回收,而是选择一部分收益高的 Old Region 和整个年轻代一起回收。
也就是说,G1 是“挑着回收”。
4. G1 的暂停时间目标不是绝对保证
你写:
STW 的时间不能超过 200ms
建议改成:
G1 会尽量让 STW 时间接近或不超过设定目标,但不是绝对保证。
因为实际停顿时间受存活对象数量、堆大小、引用关系复杂度等因素影响。
一句话总结:
CMS 是“尽量并发,减少停顿”,但容易产生碎片和 Full GC;G1 是“分区管理,优先回收垃圾最多的区域”,可以更好地控制停顿时间并减少碎片。