GC 算法与垃圾收集器演进:从 Serial 到 ZGC
垃圾收集是 Java 面试的核心战场。从最基础的标记清除到 ZGC 的染色指针,本文把 GC 的演进脉络一次讲透。
一、GC 基础:如何判断对象可回收?
1. 引用计数法(已淘汰)
// 引用计数法: 每个对象有一个引用计数器
// 但无法解决循环引用
Object a = new Object(); // a.count = 1
Object b = new Object(); // b.count = 1
a.ref = b; // b.count = 2
b.ref = a; // a.count = 2
a = null; // a.count = 1
b = null; // b.count = 1
// a 和 b 互相引用,count 永远 > 0 → 永远不会被回收!
2. 可达性分析(JVM 使用)
// 从 GC Roots 出发,沿引用链遍历
// 不可达的对象 = 可回收对象
// GC Roots 包括:
// 1. 虚拟机栈中的引用 (局部变量)
// 2. 方法区中静态变量的引用
// 3. 方法区中常量的引用
// 4. 本地方法栈 JNI 引用
// 5. Java 虚拟机内部引用 (基本类型 Class)
// 6. 同步锁持有的对象
可达性分析:
GC Roots
├── 栈引用 ──▶ objA ──▶ objB ──▶ objC (可达,不回收)
├── 静态变量 ──▶ objD (可达)
│
└── objX ──✗ (无引用链,不可达 → 可回收)
objY ──✗
objZ ──▶ objW ──✗ (objZ 可达但 objW 不可达)
3. 四种引用类型
// 强引用: 永不回收(GC Roots 可达)
Object strong = new Object();
// 软引用: 内存不足时回收
SoftReference<Object> soft = new SoftReference<>(new Object());
// 弱引用: 下次 GC 就回收
WeakReference<Object> weak = new WeakReference<>(new Object());
// 虚引用: 不影响生命周期,只做跟踪通知
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), new ReferenceQueue<>());
| 引用类型 | 回收时机 | 用途 |
|---|---|---|
| 强引用 | 永不(除非置 null) | 普通对象 |
| 软引用 | 内存不足时 | 缓存 |
| 弱引用 | 下次 GC | ThreadLocalMap、WeakHashMap |
| 虚引用 | 随时 | 跟踪对象被回收的时机 |
二、GC 算法
1. 标记-清除(Mark-Sweep)
标记-清除算法:
标记阶段: 清除阶段:
┌───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┐
│ A │ ✗ │ B │ ✗ │ C │ │ A │ │ B │ │ C │
└───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┘
(✗ = 标记为可回收) (清除后产生碎片)
- 优点:实现简单
- 缺点:产生内存碎片
2. 复制算法(Copying)
复制算法:
From 区: To 区:
┌───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┐
│ A │ ✗ │ B │ ✗ │ C │ → │ A │ B │ C │ │ │
└───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┘
(存活对象复制到 To 区) (存活对象连续排列,无碎片)
From 区整体清空
- 优点:无碎片,分配快
- 缺点:浪费一半空间
3. 标记-整理(Mark-Compact)
标记-整理算法:
标记阶段: 整理阶段:
┌───┬───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┬───┐
│ A │ ✗ │ B │ ✗ │ C │ ✗ │ │ A │ B │ C │ │ │ │
└───┴───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┴───┘
(标记可回收对象) (存活对象向一端移动,清理边界外)
- 优点:无碎片,不浪费空间
- 缺点:移动对象开销大,STW 时间长
三种算法对比
| 算法 | 碎片 | 空间利用率 | 效率 | 适用区域 |
|---|---|---|---|---|
| 标记-清除 | 有 | 100% | 中等 | 老年代(CMS) |
| 复制 | 无 | 50% | 最高 | 新生代 |
| 标记-整理 | 无 | 100% | 较低 | 老年代 |
4. 分代收集算法
分代收集策略:
新生代: 对象朝生夕死 → 复制算法
┌─────────────────────────────────┐
│ Eden │ S0 │ S1 │ │
│ 80% │ 10% │ 10% │ │
└─────────────────────────────────┘
Minor GC: Eden + 活跃 Survivor → 空闲 Survivor
每次存活 → 年龄 +1, 达到阈值晋升老年代
老年代: 对象存活率高 → 标记-清除/标记-整理
┌─────────────────────────────────┐
│ Old Gen │
│ Major GC / Full GC │
│ 标记存活对象 → 清除/整理 │
└─────────────────────────────────┘
三、垃圾收集器演进
垃圾收集器演进时间线:
JDK 1.3 ──── Serial / Serial Old
JDK 1.4 ──── Parallel Scavenge / Parallel Old (吞吐量优先)
JDK 1.5 ──── CMS (低延迟, 标记-清除)
JDK 1.7u4 ──── G1 (可预测停顿)
JDK 11 ──── ZGC (染色指针, <10ms)
JDK 12 ──── Shenandoah ( Brooks 指针, <10ms)
JDK 15 ──── ZGC 正式可用, CMS 废弃
JDK 17 ──── ZGC Generational
JDK 21 ──── Generational ZGC (分代 ZGC)
性能对比:
┌────────────────────────────────────────────────┐
│ 停顿时间(ms) │
│ │
│ Serial ████████████████████████ 500+ │
│ Parallel ████████████████ 200 │
│ CMS ██████████ 100 │
│ G1 █████ 50 │
│ ZGC █ <10 │
│ Shenandoah █ <10 │
└────────────────────────────────────────────────┘
1. Serial / Serial Old
单线程收集器,STW 时只使用一个线程
适用于: 客户端模式、小堆内存
新生代(Serial): 复制算法
老年代(Serial Old): 标记-整理
-XX:+UseSerialGC
2. Parallel Scavenge / Parallel Old
多线程收集器,关注吞吐量
适用于: 后台计算、批处理
新生代(Parallel Scavenge): 复制算法, 多线程
老年代(Parallel Old): 标记-整理, 多线程
-XX:+UseParallelGC
-XX:MaxGCPauseMillis=200 (最大停顿时间)
-XX:GCTimeRatio=99 (GC时间占比 1/(1+99)=1%)
3. CMS(Concurrent Mark Sweep)
低延迟收集器,关注停顿时间
适用于: 用户交互场景
四阶段:
┌─────────────────────────────────────────────┐
│ 1. 初始标记 (STW) │
│ 标记 GC Roots 直接关联的对象 │
│ ──────── (极短) │
│ │
│ 2. 并发标记 (并发) │
│ 沿引用链遍历, 标记所有可达对象 │
│ ──────────────────────── (较长时间) │
│ │
│ 3. 重新标记 (STW) │
│ 修正并发标记期间引用变化的对象 │
│ ────── (较短) │
│ │
│ 4. 并发清除 (并发) │
│ 清除未标记的对象 │
│ ──────────────────────── (较长时间) │
└─────────────────────────────────────────────┘
STW: 初始标记 + 重新标记 (很短)
并发: 并发标记 + 并发清除 (大部分时间)
缺点:
- 标记-清除 → 内存碎片
- 浮动垃圾 → 并发清除时新产生的垃圾
- Concurrent Mode Failure → 退化为 Serial Old
- 对 CPU 敏感 → 占用用户线程
4. G1(Garbage First)
G1 将堆分为多个 Region, 不再是物理分代
┌──────────────────────────────────────────┐
│ G1 堆内存布局 │
│ │
│ ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ │
│ │E │E │S │O │O │H │H │O │E │O │ │
│ ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ │
│ │O │E │O │S │O │E │H │O │O │S │ │
│ ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ │
│ │E │O │O │E │S │O │H │O │E │O │ │
│ └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ │
│ E=Eden S=Survivor O=Old H=Humongous │
│ │
│ Region 大小: 1-32MB (2的幂) │
│ 逻辑分代, 物理不连续 │
│ 优先回收垃圾最多的 Region (Garbage First) │
└──────────────────────────────────────────┘
// G1 四阶段
// 1. 初始标记 (STW, 搭载一次 Minor GC)
// 2. 并发标记 (并发, 跟踪 Region 中的对象)
// 3. 最终标记 (STW, 处理 SATB 记录)
// 4. 筛选回收 (STW, 按收益排序, 回收高收益 Region)
// G1 参数
// -XX:+UseG1GC
// -XX:MaxGCPauseMillis=200 // 目标停顿时间
// -XX:G1HeapRegionSize=16m // Region 大小
// -XX:InitiatingHeapOccupancyPercent=45 // 触发并发标记的堆占用率
G1 vs CMS:
CMS: 整个老年代一起回收, 不可控
G1: 按 Region 回收, 可选停顿时间
CMS: 标记-清除 (碎片)
G1: 复制+标记-整理 (Region间复制, 无碎片)
CMS: 停顿时间不可预测
G1: -XX:MaxGCPauseMillis 可预测
5. ZGC(Z Garbage Collector)
ZGC: 染色指针 + 读屏障, 停顿 < 10ms
核心技术:
┌────────────────────────────────────────┐
│ 1. 染色指针 (Colored Pointers) │
│ 64位指针中用4位存储GC信息 │
│ ┌────────────────────────────────┐ │
│ │ 18位 unused │ 1 │ Finalizable │ │
│ │ │ 1 │ Remapped │ │
│ │ │ 1 │ Marked0 │ │
│ │ │ 1 │ Marked1 │ │
│ │ 42位地址空间 │ │
│ └────────────────────────────────┘ │
│ │
│ 2. 读屏障 (Load Barrier) │
│ 读取对象引用时自动检查颜色 │
│ 如果颜色不对 → 修正指针 │
│ 对象转移和重定位对应用透明 │
│ │
│ 3. 并发转移 │
│ 标记和转移都在并发执行 │
│ STW 只有三个阶段, 每个 < 1ms │
└────────────────────────────────────────┘
ZGC 工作流程:
┌─────────────┐ STW(<1ms) ┌─────────────┐
│ 初始标记 │─────────────▶│ 并发标记 │ 并发
└─────────────┘ └──────┬──────┘
│
┌───────────────────────────┘
▼
┌─────────────┐ STW(<1ms) ┌─────────────┐
│ 再定位根集 │─────────────▶│ 并发转移 │ 并发
└─────────────┘ └──────┬──────┘
│
┌───────────────────────────┘
▼
┌─────────────┐ STW(<1ms) ┌─────────────┐
│ 再定位集 │─────────────▶│ 并发重映射 │ 并发
└─────────────┘ └─────────────┘
总停顿: 3次 STW, 每次 < 1ms
可处理 TB 级堆内存
6. Shenandoah
Shenandoah: Brooks 指针 + 并发整理
与 ZGC 区别:
- ZGC: 染色指针 (64位指针存GC信息)
- Shenandoah: Brooks 转发指针 (每个对象额外一个指针)
对象内存布局:
┌───────────────────┐
│ Brooks Pointer │ ──▶ 新地址 (对象移动后)
├───────────────────┤
│ 对象头 │
├───────────────────┤
│ 实例数据 │
└───────────────────┘
四、收集器对比总表
| 收集器 | 模式 | 算法 | STW | 目标 | 适用堆 |
|---|---|---|---|---|---|
| Serial | 单线程 | 复制 | 长 | 简单 | < 100MB |
| Parallel | 多线程 | 复制+整理 | 中 | 吞吐量 | < 4GB |
| CMS | 并发 | 标记-清除 | 短(有碎片) | 低延迟 | < 8GB |
| G1 | 并发 | 复制+整理 | 可控 | 可预测停顿 | 4-32GB |
| ZGC | 并发 | 染色指针 | <10ms | 超低延迟 | 8GB-16TB |
| Shenandoah | 并发 | Brooks指针 | <10ms | 超低延迟 | 4GB-32GB |
选型指南:
堆 < 100MB: Serial
堆 < 4GB, 关注吞吐量: Parallel
堆 < 8GB, 关注延迟: CMS (JDK 14已废弃) → G1
堆 4-32GB: G1 (JDK 9+ 默认)
堆 > 8GB, 低延迟: ZGC (JDK 15+)
五、常见面试追问
Q1:为什么 CMS 使用标记-清除而不是标记-整理?
CMS 追求低延迟,标记-整理需要移动对象,移动期间需要 STW(或更新所有引用),无法并发执行。标记-清除不需要移动对象,可以并发清除。代价是产生碎片。
Q2:G1 的 Mixed GC 是什么?
G1 的 Mixed GC 会回收新生代和部分老年代 Region。G1 优先回收垃圾最多的 Region(Garbage First),所以可以选择性地回收老年代中垃圾最多的 Region,而不是整个老年代。
Q3:ZGC 如何做到 < 10ms 停顿?
- 染色指针:指针中存储 GC 状态,无需遍历对象
- 读屏障:读取引用时自动修复指针,转移可并发
- 并发转移:标记和转移都在并发执行,STW 只有初始标记阶段
Q4:Full GC 和 Major GC 的区别?
- Minor GC: 回收新生代(频繁,快)
- Major GC: 回收老年代(通常伴随 Full GC)
- Full GC: 回收整个堆 + 元空间(慢,应尽量避免)
Q5:什么是安全点(Safepoint)?
Safepoint 是程序执行中的"安全暂停点",JVM 在这些点可以安全执行 GC。Safepoint 通常在方法调用、循环回边等位置。JIT 编译器会在这些位置插入 safepoint 轮询指令。
六、总结
GC 演进核心: 减少 STW 时间
标记清除 → 复制 → 标记整理 → 分代收集
↓
Serial → Parallel → CMS(并发) → G1(Region化) → ZGC(染色指针)
算法: 新生代用复制(对象朝生夕死)
老年代用标记-整理(对象存活率高)
收集器: 吞吐量 → Parallel, 延迟 → G1/ZGC
GC 的演进方向始终是 减少停顿时间 和 支持更大堆内存,从 Serial 的数百毫秒到 ZGC 的亚毫秒级,Java 在低延迟方向上不断突破。