JVM 垃圾回收(GC)
基于 JDK 8+ 的 JVM 垃圾回收机制深度解析。
一、GC 核心问题
GC 要解决三个核心问题:
| 问题 | 说明 |
|---|---|
| 哪些内存需要回收? | 判断对象是否存活 |
| 什么时候回收? | GC 触发时机 |
| 如何回收? | 垃圾回收算法和收集器 |
二、如何判断对象是否存活
2.1 引用计数法(Reference Counting)—— JVM 不用
原理:给对象添加引用计数器,引用时 +1,引用失效时 -1,为 0 时可回收。
致命缺陷:无法解决循环引用问题。
对象 A 引用对象 B
对象 B 引用对象 A
两者计数永远不为 0,无法回收!2.2 可达性分析(Reachability Analysis)—— JVM 使用

原理:从 GC Roots 出发,沿引用链遍历,不可达的对象即为垃圾。
GC Roots
│
▼
┌───────┐
│ 对象A │ ← 可达,存活
└───┬───┘
│
▼
┌───────┐ ┌───────┐
│ 对象B │────►│ 对象C │ ← 可达,存活
└───────┘ └───────┘
┌───────┐ ┌───────┐
│ 对象X │◄───►│ 对象Y │ ← 不可达,可回收(即使互相引用)
└───────┘ └───────┘2.3 GC Roots 有哪些?
| GC Roots 类型 | 说明 |
|---|---|
| 虚拟机栈中的引用 | 栈帧中局部变量表里的对象引用 |
| 方法区的静态变量 | static 修饰的引用类型变量 |
| 方法区的常量 | final 修饰的引用类型常量 |
| 本地方法栈中的 JNI 引用 | Native 方法持有的对象引用 |
| 同步锁持有的对象 | synchronized 锁住的对象 |
| JVM 内部引用 | 基本类型的 Class 对象、常驻异常对象、类加载器 |
三、四种引用类型
按引用强度从强到弱排列:
| 引用类型 | 回收时机 | 用途 | 代码示例 |
|---|---|---|---|
| 强引用 | 永不回收(除非置 null) | 普通对象引用 | Object obj = new Object() |
| 软引用 | 内存不足时回收 | 缓存 | SoftReference<Object> |
| 弱引用 | 下次 GC 时回收 | WeakHashMap | WeakReference<Object> |
| 虚引用 | 随时可回收,无法获取对象 | 跟踪对象回收、堆外内存释放 | PhantomReference<Object> |
// 软引用示例 - 适合做缓存
SoftReference<byte[]> cache = new SoftReference<>(new byte[1024 * 1024]);
// 弱引用示例
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 虚引用示例 - 必须配合 ReferenceQueue 使用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);四、垃圾回收算法

4.1 标记-清除(Mark-Sweep)
【标记前】
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │ F │ G │ H │
└───┴───┴───┴───┴───┴───┴───┴───┘
【标记后】(标记存活对象)
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ X │ C │ X │ E │ X │ X │ H │ (X = 垃圾)
│ 活 │ │ 活 │ │ 活 │ │ │ 活 │
└───┴───┴───┴───┴───┴───┴───┴───┘
【清除后】
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ │ C │ │ E │ │ │ H │ ← 产生内存碎片!
└───┴───┴───┴───┴───┴───┴───┴───┘| 优点 | 缺点 |
|---|---|
| 实现简单 | 效率不高(需要遍历两次) |
| 产生内存碎片 |
4.2 复制算法(Copying)
【复制前】
From 区 To 区(空)
┌───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┐
│ A │ X │ C │ X │ E │ │ │ │ │ │ │ (X = 垃圾)
└───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┘
【复制后】(只复制存活对象)
From 区(清空) To 区
┌───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┐
│ │ │ │ │ │ │ A │ C │ E │ │ │ ← 紧凑排列!
└───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┘| 优点 | 缺点 |
|---|---|
| 无内存碎片 | 浪费一半内存空间 |
| 效率高(只遍历存活对象) |
适用场景:新生代(存活率低,复制对象少)
4.3 标记-整理(Mark-Compact)
【标记后】
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ X │ C │ X │ E │ X │ X │ H │ (X = 垃圾)
└───┴───┴───┴───┴───┴───┴───┴───┘
【整理后】(存活对象向一端移动)
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ C │ E │ H │ │ │ │ │ ← 紧凑排列!
└───┴───┴───┴───┴───┴───┴───┴───┘| 优点 | 缺点 |
|---|---|
| 无内存碎片 | 需要移动对象,效率较低 |
适用场景:老年代(存活率高,不适合复制)
4.4 分代收集(设计思想)
核心思想:根据对象存活周期不同,划分不同区域,采用不同算法。
| 区域 | 特点 | 使用算法 |
|---|---|---|
| 新生代 | 存活率低(~2%) | 复制算法 |
| 老年代 | 存活率高(~90%) | 标记-清除 或 标记-整理 |
注意:分代收集是设计思想,不是具体算法。它是对复制、标记-清除、标记-整理等算法的组合运用。
五、对象分配与晋升流程
5.1 对象分配流程
new 对象
│
▼
┌───────────────┐ 是 ┌─────────────────┐
│ 是否大对象? │───────────►│ 直接进入老年代 │
└───────┬───────┘ └─────────────────┘
│ 否
▼
┌───────────────┐ 成功 ┌─────────────────┐
│ 尝试 TLAB 分配│───────────►│ 分配完成 │
└───────┬───────┘ └─────────────────┘
│ 失败
▼
┌───────────────┐ 成功 ┌─────────────────┐
│ 在 Eden 分配 │───────────►│ 分配完成 │
└───────┬───────┘ └─────────────────┘
│ 失败
▼
┌───────────────┐
│ 触发 Minor GC │
└───────────────┘5.2 对象晋升老年代条件
| 条件 | 说明 | 参数 |
|---|---|---|
| 年龄达到阈值 | 默认 15 次 Minor GC 后晋升 | -XX:MaxTenuringThreshold=15 |
| 大对象直接分配 | 超过指定大小的对象 | -XX:PretenureSizeThreshold |
| 动态年龄判定 | Survivor 中相同年龄对象总和 > Survivor 空间一半 | - |
| Survivor 放不下 | Minor GC 后存活对象太多 | - |
六、GC 类型
| GC 类型 | 回收范围 | 触发条件 |
|---|---|---|
| Minor GC | 新生代 | Eden 区满 |
| Major GC | 老年代 | 老年代空间不足(仅 CMS 单独回收老年代) |
| Full GC | 整个堆 + 元空间 | 见下方触发条件 |
Full GC 触发条件
- 调用
System.gc()(建议,不保证执行) - 老年代空间不足
- 元空间不足
- Minor GC 晋升到老年代的平均大小 > 老年代剩余空间(分配担保失败)
- CMS GC 时 Concurrent Mode Failure
七、垃圾收集器概览
7.1 收集器分类
| 收集器 | 类型 | 算法 | 特点 | 适用场景 |
|---|---|---|---|---|
| Serial | 单线程 | 复制 | STW,简单高效 | 客户端、小内存 |
| ParNew | 多线程 | 复制 | Serial 的多线程版 | 配合 CMS |
| Parallel Scavenge | 多线程 | 复制 | 吞吐量优先 | 后台计算任务 |
| Serial Old | 单线程 | 标记-整理 | STW | CMS 后备、客户端 |
| Parallel Old | 多线程 | 标记-整理 | 吞吐量优先 | 配合 Parallel Scavenge |
| CMS | 并发 | 标记-清除 | 低停顿 | Web 服务器 |
| G1 | 并发+分区 | 复制+标记-整理 | 可控停顿时间 | 大内存、低延迟 |
7.2 搭配使用关系
新生代收集器 老年代收集器
───────────── ─────────────
Serial ───────────────► Serial Old
ParNew ───────────────► CMS / Serial Old
Parallel Scavenge ────────────► Parallel Old / Serial Old
G1(整堆收集,不区分新生代老年代收集器)八、CMS 收集器详解

8.1 基本信息
| 属性 | 说明 |
|---|---|
| 全称 | Concurrent Mark Sweep |
| 收集区域 | 老年代 |
| 使用算法 | 标记-清除 |
| 设计目标 | 最短停顿时间 |
| 搭配使用 | ParNew(新生代) |
| 启用参数 | -XX:+UseConcMarkSweepGC |
8.2 四个阶段
用户线程 ═══════════════════════════════════════════════════════════════════
| | | |
| | | |
v v v v
+---------+ +---------+
| STW | 并发运行 | STW | 并发运行
+---------+ +---------+
| |
v v
+-------------+ +-----------------+ +-------------+ +-----------------+
| 1.初始标记 | | 2.并发标记 | | 3.重新标记 | | 4.并发清除 |
| Initial | | Concurrent | | Remark | | Concurrent |
| Mark | | Mark | | | | Sweep |
| | | | | | | |
| STW! | | 与用户线程并发 | | STW! | | 与用户线程并发 |
| 速度:很快 | | 速度:最慢 | | 速度:较快 | | 速度:较快 |
+-------------+ +-----------------+ +-------------+ +-----------------+
时间占比: [5%] [70%] [10%] [15%]阶段 1:初始标记(Initial Mark)[STW]
做什么:仅标记 GC Roots 直接关联的对象
特点:需要 STW,但时间很短(只扫描一层)
阶段 2:并发标记(Concurrent Mark)
做什么:从初始标记的对象出发,遍历整个对象图,标记所有可达对象
特点:
- 与用户线程并发执行
- 耗时最长
- 会产生漏标问题
阶段 3:重新标记(Remark)[STW]
做什么:修正并发标记期间因用户程序变动而产生的标记变化
技术:增量更新(Incremental Update)
阶段 4:并发清除(Concurrent Sweep)
做什么:清除未被标记的垃圾对象
特点:
- 与用户线程并发执行
- 使用标记-清除算法 → 产生内存碎片
- 并发期间产生的垃圾需要下次 GC 回收(浮动垃圾)
8.3 CMS 的缺点
| 问题 | 说明 |
|---|---|
| CPU 敏感 | 并发阶段占用 CPU 资源 |
| 浮动垃圾 | 并发清除期间产生的新垃圾本次无法回收 |
| 内存碎片 | 标记-清除算法导致 |
| Concurrent Mode Failure | 并发期间老年代空间不足,退化为 Serial Old |
8.4 Concurrent Mode Failure
定义:CMS 并发收集期间,老年代空间不足以容纳新晋升的对象
后果:退化为 Serial Old 进行 Full GC,长时间 STW
预防:-XX:CMSInitiatingOccupancyFraction=70(更早触发 CMS)
九、G1 收集器详解
9.1 基本信息
| 属性 | 说明 |
|---|---|
| 全称 | Garbage First |
| 收集区域 | 整个堆(Region 分区) |
| 使用算法 | 标记-整理 + 复制 |
| 设计目标 | 可预测的停顿时间 |
| 启用参数 | -XX:+UseG1GC |
| 目标停顿 | -XX:MaxGCPauseMillis=200 |
9.2 Region 内存布局
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ E │ E │ S │ O │ O │ H │ H │ E │ O │ │ S │ O │
├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
│ O │ │ E │ O │ E │ E │ O │ O │ S │ O │ │ E │
├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
│ O │ O │ O │ │ E │ O │ │ O │ O │ E │ E │ O │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
E = Eden S = Survivor O = Old H = Humongous(大对象)
空白 = 空闲
每个 Region 大小:1MB ~ 32MB(2 的幂次)9.3 核心特点
| 特点 | 说明 |
|---|---|
| 分区收集 | 将堆划分为多个 Region,可单独回收 |
| 可预测的停顿 | 通过 -XX:MaxGCPauseMillis 设置目标停顿时间 |
| 优先回收价值高的 Region | 垃圾最多的 Region 优先回收(Garbage First) |
| Mixed GC | 可以同时回收新生代和部分老年代 |
9.4 G1 四个阶段
1. 初始标记 (Initial Mark) ─── STW
2. 并发标记 (Concurrent Mark) ─── 并发
3. 最终标记 (Final Mark) ─── STW(处理 SATB 队列)
4. 筛选回收 (Cleanup/Evacuation)─── STW(移动存活对象到新 Region)9.5 CMS vs G1 对比
| 对比项 | CMS | G1 |
|---|---|---|
| 最后阶段 | 并发清除(并发) | 筛选回收(STW) |
| 算法 | 标记-清除 | 复制(Evacuation) |
| 碎片 | 有 | 无 |
| 停顿可控 | 不可控 | 可控 |
| 解决漏标 | 增量更新 | SATB |
| 状态 | JDK 9 废弃 | 推荐使用 |
十、三色标记法

10.1 三种颜色定义
| 颜色 | 状态 | 说明 |
|---|---|---|
| 白色 (W) | 未访问 | 未被标记的对象,GC 结束后会被回收 |
| 灰色 (G) | 正在处理 | 已被标记,但引用的对象还未全部扫描 |
| 黑色 (B) | 已完成 | 已被标记,且引用的对象都已扫描完 |
10.2 标记过程
【初始状态】所有对象都是白色 (W=白色, G=灰色, B=黑色)
GC Roots → A(W) → B(W) → C(W)
【步骤 1】GC Roots 直接引用的对象变灰色
GC Roots → A(G) → B(W) → C(W)
【步骤 2】处理灰色 A,扫描 A 的引用,B 变灰,A 变黑
GC Roots → A(B) → B(G) → C(W)
【步骤 3】处理灰色 B,扫描 B 的引用,C 变灰,B 变黑
GC Roots → A(B) → B(B) → C(G)
【步骤 4】处理灰色 C,C 无引用,C 变黑
GC Roots → A(B) → B(B) → C(B)
【结束】没有灰色对象,剩余白色对象就是垃圾10.3 三色标记不变式
黑色对象不能直接指向白色对象
如果违反这个不变式,会导致漏标(存活对象被误回收)。
十一、漏标问题与解决方案
11.1 漏标场景
【初始状态】GC 正在并发标记 (W=白色, G=灰色, B=黑色)
A(B) → B(G) → C(W)
【用户线程操作】同时执行:
1. A.field = C (黑色 A 新增对白色 C 的引用)
2. B.field = null (删除 B 到 C 的引用)
【结果】
A(B) ────────────> C(W)
(新增)
B(B) X C(W)
(删除)
【问题】
- A 是黑色,不会再被扫描,A→C 不会被发现
- B 扫描时,B→C 已删除,也不会发现 C
- C 仍是白色,会被当成垃圾回收
- 但 C 实际上是存活的!这会导致程序错误!11.2 漏标的两个必要条件
只有同时满足以下两个条件,才会发生漏标:
| 条件 | 说明 |
|---|---|
| 条件一 | 黑色对象新增了对白色对象的引用 |
| 条件二 | 灰色对象删除了对该白色对象的所有引用路径 |
打破任意一个条件,就能解决漏标问题!
11.3 两种解决方案
| 方案 | 打破条件 | 使用屏障 | 使用收集器 |
|---|---|---|---|
| 增量更新 | 条件一 | 写后屏障 | CMS |
| 原始快照 (SATB) | 条件二 | 写前屏障 | G1 |
十二、写屏障详解
12.1 什么是写屏障?
写屏障 = 在引用赋值操作前后插入的特殊代码
// 源代码
a.field = c;
// 加入写屏障后(伪代码)
writeBarrier_pre(a, field, c); // 写前屏障
a.field = c;
writeBarrier_post(a, field, c); // 写后屏障12.2 增量更新(CMS 使用)
原理:当黑色对象新增对白色对象的引用时,记录下来,重新标记阶段重新扫描。
// 写后屏障伪代码
void writeBarrier_post(Object obj, Field field, Object value) {
if (isBlack(obj) && isWhite(value)) {
// 黑色对象引用了白色对象,记录下来
recordForRescan(obj);
}
}重新标记时:从记录的黑色对象重新扫描
12.3 原始快照 SATB(G1 使用)
SATB = Snapshot At The Beginning
原理:当灰色对象删除对白色对象的引用时,将被删除的旧值记录下来,保证该对象仍会被标记。
// 写前屏障伪代码
void writeBarrier_pre(Object obj, Field field, Object newValue) {
Object oldValue = obj.field; // 获取旧值
if (oldValue != null && isWhite(oldValue)) {
// 旧值是白色对象,记录下来
satbQueue.push(oldValue);
}
}SATB 队列内容:被删除引用的旧值对象引用
最终标记时:遍历 SATB 队列,将队列中的对象及其引用链标记为存活
12.4 SATB 的含义
标记开始时存活的对象,就认为它存活(即使后来变成垃圾也不回收)
结果:可能产生浮动垃圾,下次 GC 回收
12.5 CMS vs G1 写屏障对比
| 对比项 | CMS | G1 |
|---|---|---|
| 方案 | 增量更新 | SATB |
| 屏障类型 | 写后屏障 | 写前屏障 |
| 记录内容 | 新增引用的黑色对象 | 被删除引用的旧值 |
| 重新标记 | 从黑色对象重新扫描 | 处理 SATB 队列 |
| 浮动垃圾 | 较少 | 可能较多 |
| 停顿时间 | 重新标记较长 | 最终标记较短 |
十三、安全点与安全区域
13.1 安全点(Safepoint)
定义:程序执行时并非任意位置都能暂停进行 GC,只有到达安全点才可以。
安全点选取位置:
- 方法调用
- 循环跳转
- 异常跳转
让线程到达安全点的方式:
- 主动式中断:设置标志位,线程轮询到标志时主动挂起
13.2 安全区域(Safe Region)
定义:线程处于 Sleep 或 Blocked 状态时,无法主动跑到安全点,此时需要安全区域。
特点:引用关系不会发生变化的代码区域。
十四、常用 GC 参数
14.1 堆内存参数
| 参数 | 说明 |
|---|---|
-Xms | 堆初始大小 |
-Xmx | 堆最大大小 |
-Xmn | 新生代大小 |
-XX:SurvivorRatio=8 | Eden:S0:S1 = 8:1:1 |
-XX:MaxTenuringThreshold=15 | 晋升年龄阈值 |
14.2 收集器选择参数
| 参数 | 说明 |
|---|---|
-XX:+UseSerialGC | 使用 Serial + Serial Old |
-XX:+UseParNewGC | 使用 ParNew + Serial Old |
-XX:+UseConcMarkSweepGC | 使用 ParNew + CMS |
-XX:+UseG1GC | 使用 G1 |
-XX:MaxGCPauseMillis=200 | G1 目标停顿时间 |
14.3 CMS 参数
| 参数 | 说明 |
|---|---|
-XX:CMSInitiatingOccupancyFraction=N | 老年代占用 N% 时触发 CMS |
-XX:+UseCMSCompactAtFullCollection | Full GC 时压缩整理 |
-XX:CMSFullGCsBeforeCompaction=N | N 次 Full GC 后压缩 |
14.4 调试参数
| 参数 | 说明 |
|---|---|
-XX:+PrintGCDetails | 打印 GC 详情 |
-XX:+HeapDumpOnOutOfMemoryError | OOM 时生成堆转储 |
-XX:HeapDumpPath=/path/to/dump | 堆转储文件路径 |
十五、GC 面试高频问题
| 问题 | 答案要点 |
|---|---|
| 如何判断对象可回收? | 可达性分析、GC Roots |
| GC Roots 有哪些? | 栈帧引用、静态变量、常量、JNI 引用等 |
| 垃圾回收算法? | 标记-清除、复制、标记-整理、分代收集(思想) |
| 为什么新生代用复制算法? | 存活率低,复制开销小 |
| CMS 四个阶段? | 初始标记(STW)→并发标记→重新标记(STW)→并发清除 |
| G1 四个阶段? | 初始标记(STW)→并发标记→最终标记(STW)→筛选回收(STW) |
| CMS 和 G1 区别? | 分区 vs 分代、可控停顿、碎片处理、解决漏标方式 |
| 什么是三色标记? | 白(未访问)、灰(处理中)、黑(完成) |
| 什么是漏标? | 黑色引用白色 + 灰色删除白色引用 |
| CMS 如何解决漏标? | 增量更新 + 写后屏障 |
| G1 如何解决漏标? | SATB + 写前屏障 |
| 什么时候触发 Full GC? | 老年代满、元空间满、分配担保失败等 |
| 对象什么时候进入老年代? | 年龄、大对象、动态年龄判定、Survivor 放不下 |
| 什么是 Concurrent Mode Failure? | CMS 并发期间老年代空间不足,退化为 Serial Old |
