【JVM】垃圾回收器

可以把 CMS 和 G1 理解成两代不同思想的垃圾回收器:

CMS 的核心目标是:尽量缩短单次 STW 时间。

G1 的核心目标是:在可控停顿时间内,尽量回收更多垃圾。


一、CMS 垃圾回收器

CMS 全称是 Concurrent Mark Sweep ,中文一般叫 并发标记清除垃圾回收器

它主要用于 老年代回收

它的设计目标是:

响应时间优先,尽量减少 STW 时间。

也就是说,CMS 更关心的是:

用户线程不要停太久。

比如一些 Web 系统、接口服务,如果一次 GC 停顿几秒,用户请求就会明显卡顿,所以 CMS 希望把 GC 的大部分工作和用户线程并发执行。


二、CMS 的回收过程

CMS 的完整过程主要有四步:

text 复制代码
初始标记 -> 并发标记 -> 重新标记 -> 并发清理

1. 初始标记:STW

初始标记会暂停用户线程。

它只标记一小部分对象:

GC Roots 能直接关联到的对象。

比如:

text 复制代码
线程栈中的引用
静态变量引用
常量引用
JNI 引用

这些根对象直接能找到的对象,会被先标记出来。

因为这个阶段只标记直接关联对象,所以速度很快,STW 时间较短。


2. 并发标记:不需要 STW

并发标记阶段,GC 线程和用户线程同时执行。

它会从初始标记阶段找到的对象继续往下遍历,找到所有可达对象。

比如:

text 复制代码
GC Roots
   ↓
对象 A
   ↓
对象 B
   ↓
对象 C

如果 A、B、C 都能从 GC Roots 间接访问到,那它们都不是垃圾。

这个阶段耗时较长,但因为它和用户线程并发执行,所以用户程序不会完全停下来。

这就是 CMS 停顿时间短的重要原因之一。


3. 重新标记:STW

问题来了。

并发标记的时候,用户线程还在运行。

用户线程可能会修改对象之间的引用关系。

比如原来是:

text 复制代码
A -> B

并发标记过程中,用户线程改成了:

text 复制代码
A -> C

这时候 GC 线程之前看到的对象关系可能已经过时了。

所以 CMS 需要进入 重新标记阶段,再次暂停用户线程,修正并发标记期间发生变化的引用关系。

你写的"漏标"这个理解是对的,但可以说得更准确一点:

重新标记主要是为了修正并发标记期间,由于用户线程继续运行导致的标记变化,避免把仍然存活的对象误判为垃圾。

注意:GC 最怕的是 把活对象当垃圾回收掉,这会导致程序错误。


4. 并发清理:不需要 STW

标记完成后,哪些对象是活的,哪些对象是垃圾,就已经知道了。

然后 CMS 会清理那些没有被标记的对象。

这个阶段也是并发执行的,GC 线程和用户线程同时工作。

但是 CMS 使用的是 标记-清除算法

所以它只是把垃圾对象占用的空间释放掉,并不会移动存活对象。


三、CMS 为什么 STW 时间短?

主要有两个原因。

第一,CMS 把耗时最长的两个阶段做成了并发:

text 复制代码
并发标记
并发清理

这两个阶段不需要长时间暂停用户线程。

第二,CMS 使用的是 标记-清除算法

标记-清除不需要移动对象,只需要把垃圾对象清掉,所以清理速度相对较快。

所以 CMS 的整体特点是:

text 复制代码
停顿时间短
响应速度好
适合低延迟系统

四、CMS 的缺点

CMS 的缺点也很明显,主要有三个。


1. 内存碎片问题

CMS 使用的是 标记-清除算法

标记-清除不会整理内存。

比如老年代原来是这样的:

text 复制代码
[对象][垃圾][对象][垃圾][对象][垃圾]

清理之后变成:

text 复制代码
[对象][空闲][对象][空闲][对象][空闲]

虽然空闲空间很多,但这些空间是不连续的。

这就会产生内存碎片。

假设现在有很多小空闲块:

text 复制代码
2MB + 3MB + 4MB + 5MB

总共 14MB 空间。

但是如果你要分配一个 10MB 的大对象,就可能失败,因为没有一块连续的 10MB 空间。

于是就可能触发 Full GC。


2. 浮动垃圾问题

CMS 在并发清理阶段,用户线程还在运行。

用户线程运行过程中,还可能继续产生新的垃圾。

这些垃圾是在 CMS 标记完成之后才产生的,所以本轮 CMS 没有办法处理它们。

这些垃圾就叫:

浮动垃圾。

比如:

text 复制代码
CMS 已经完成标记
用户线程继续运行
用户线程又产生了一批垃圾

这批垃圾只能等下一次 GC 再回收。

如果浮动垃圾太多,而老年代空间又不足,就可能出现:

Concurrent Mode Failure,并发模式失败。

这时候 CMS 就撑不住了,需要退化为 Full GC。


3. 并发执行会抢 CPU

CMS 的并发标记和并发清理虽然不会完全暂停用户线程,但 GC 线程和用户线程同时运行,会抢 CPU 资源。

如果服务器 CPU 核数比较少,CMS 可能会影响用户线程的执行效率。

所以 CMS 不是完全没有代价的。


五、CMS 退化为 Serial Old 的问题

当 CMS 出现问题,比如:

text 复制代码
老年代空间不足
浮动垃圾太多
内存碎片严重
大对象分配失败

就可能触发 Full GC。

CMS 的 Full GC 通常会退化为 Serial Old

Serial Old 是单线程老年代垃圾回收器。

它会:

text 复制代码
STW
单线程回收
使用标记-整理算法

标记-整理算法会移动对象,整理内存碎片。

比如:

text 复制代码
[对象][空闲][对象][空闲][对象]

整理后变成:

text 复制代码
[对象][对象][对象][空闲][空闲]

这样可以解决内存碎片问题。

但是代价是:

STW 时间非常长。

所以 CMS 的最大风险就是:

平时停顿很短,但一旦 Full GC,停顿可能非常严重。


六、G1 垃圾回收器

G1 全称是 Garbage First

意思是:

优先回收垃圾最多的区域。

G1 的设计目标是:

text 复制代码
低延迟
高吞吐
可预测停顿时间
适合大堆内存

和 CMS 不同,G1 不只是老年代回收器,它是一个面向整个堆的垃圾回收器。


七、G1 的堆内存结构

传统垃圾回收器一般把堆分成:

text 复制代码
新生代
老年代

比如:

text 复制代码
Eden + Survivor + Old

而 G1 把整个堆切成很多个大小相等的小块。

这些小块叫:

Region。

比如整个堆被切成这样:

text 复制代码
Region 1
Region 2
Region 3
Region 4
Region 5
...

每个 Region 可以动态扮演不同角色:

text 复制代码
Eden Region
Survivor Region
Old Region
Humongous Region

Humongous Region 用来存放大对象。

所以 G1 不是完全不要分代,而是:

逻辑上仍然有新生代、老年代,但物理上不再是连续的大块空间,而是由一个个 Region 组成。

这一点很重要。

你写的"分区取代分代"可以稍微修正成:

G1 不是取消分代,而是用 Region 作为基本管理单位,让新生代和老年代都由一组不连续的 Region 组成。


八、G1 为什么可以控制 STW 时间?

G1 有一个很重要的参数:

text 复制代码
-XX:MaxGCPauseMillis

比如设置为:

text 复制代码
200ms

意思是希望 GC 停顿时间尽量控制在 200ms 左右。

注意,是"尽量",不是绝对保证。

G1 的做法是:

不一定每次都回收整个堆,而是选择部分收益最高的 Region 回收。

比如现在有这些 Region:

text 复制代码
Region A:垃圾 80%
Region B:垃圾 70%
Region C:垃圾 20%
Region D:垃圾 10%

如果暂停时间预算有限,G1 会优先选择:

text 复制代码
Region A
Region B

因为它们垃圾最多,回收收益最高。

这就是 Garbage First 的含义:

垃圾最多的 Region 优先回收。

所以 G1 的思路是:

text 复制代码
有限的停顿时间内
选择最值得回收的 Region
尽量回收更多空间

九、G1 的回收过程

G1 的回收过程可以分成两类:

text 复制代码
Young GC
Mixed GC

你写的那套过程更接近 G1 的并发标记周期和混合回收过程。

整体可以这样理解:

text 复制代码
初始标记 -> 并发标记 -> 最终标记 -> 筛选回收/混合回收

1. 初始标记:STW

初始标记也是标记 GC Roots 直接关联的对象。

这个阶段需要 STW,但时间很短。

在 G1 中,初始标记通常会借助一次 Young GC 一起完成。


2. 并发标记:不需要 STW

并发标记阶段,GC 线程和用户线程一起执行。

它会扫描整个堆,找出存活对象,并统计每个 Region 的垃圾比例。

比如:

text 复制代码
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。

比如:

text 复制代码
Eden Region
Survivor Region
Old Region
Humongous Region

G1 会选择其中一部分 Region 进行回收。


十、G1 使用什么垃圾回收算法?

G1 整体可以理解为:

text 复制代码
局部复制算法
整体标记-整理思想

在具体回收某些 Region 时,G1 会把这些 Region 中还活着的对象复制到新的空 Region 中。

然后直接清空原来的 Region。

比如:

text 复制代码
回收前:

Region A:
[活对象][垃圾][活对象][垃圾]

复制存活对象到 Region B:

Region B:
[活对象][活对象]

然后清空 Region A。

这样做的好处是:

text 复制代码
不会产生 CMS 那种严重内存碎片

因为存活对象被复制到了新的连续空间,原 Region 可以整体释放。


十一、G1 相比 CMS 的优势

1. 解决内存碎片问题

CMS 用标记-清除,不移动对象,所以容易产生碎片。

G1 回收 Region 时,会复制存活对象,然后整体清空旧 Region。

所以 G1 可以减少内存碎片。


2. 可预测停顿时间

CMS 的目标是减少停顿,但不太好控制每次停顿时间。

G1 可以根据暂停时间目标选择回收哪些 Region。

所以 G1 更强调:

可预测的停顿时间。


3. 更适合大堆

CMS 在大堆场景下容易出现碎片和 Full GC 问题。

G1 把堆划分成 Region,可以按区域回收,所以更适合大堆内存场景。


十二、G1 的缺点

G1 也不是完美的。

它的缺点主要是:

text 复制代码
实现复杂
维护 Region 信息成本高
需要 Remembered Set 记录跨 Region 引用
小堆场景下不一定比传统回收器更快

因为 G1 是按 Region 回收的,会遇到一个问题:

如果只回收某几个 Region,那怎么知道其他 Region 有没有引用这里面的对象?

比如:

text 复制代码
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 动态组成。

也就是说:

text 复制代码
不是没有新生代和老年代
而是新生代和老年代不再是连续的大块内存

3. G1 的混合回收不是一次回收所有区域

你写得基本对,但要强调:

Mixed GC 不是把所有老年代都回收,而是选择一部分收益高的 Old Region 和整个年轻代一起回收。

也就是说,G1 是"挑着回收"。


4. G1 的暂停时间目标不是绝对保证

你写:

STW 的时间不能超过 200ms

建议改成:

G1 会尽量让 STW 时间接近或不超过设定目标,但不是绝对保证。

因为实际停顿时间受存活对象数量、堆大小、引用关系复杂度等因素影响。


一句话总结:

CMS 是"尽量并发,减少停顿",但容易产生碎片和 Full GC;G1 是"分区管理,优先回收垃圾最多的区域",可以更好地控制停顿时间并减少碎片。

相关推荐
思麟呀2 小时前
C++11并发编程:条件变量
java·linux·jvm·c++·windows
未若君雅裁3 小时前
JVM 是什么:组成、运行流程与整体架构
jvm·架构
light blue bird3 小时前
3C 数码电子BOM 协同工作台组件
java·开发语言·jvm·windows·.net·桌面端
wuminyu15 小时前
Java锁机制之轻量级锁判断与尝试逻辑源码剖析
java·linux·c语言·jvm·c++
DO your like17 小时前
CMS场景YGC失败导致FULL GC的总结
jvm
墨痕无声1 天前
JVM(六)
jvm
右耳朵猫AI1 天前
Java/JVM周刊2026W21 | Java 26发布、JDK 27抢先体验、Spring Boot 4.1预告、GlassFish 8.0.2发布
java·jvm·spring boot
小马爱打代码1 天前
系统设计:JVM Full GC 预测与自动规避系统设计
jvm
磊 子2 天前
C++function与bind绑定器讲解
java·jvm·c++