ZGC 与 Shenandoah:低延迟收集器的新时代
当 G1 还在为可控的停顿时间努力时,ZGC 和 Shenandoah 已经将目标锁定在亚毫秒级停顿。
这不仅仅是数字的进步,而是垃圾收集器设计的范式转变。
传统收集器的瓶颈
为什么 Stop The World 是问题?
┌─────────────────────────────────────────────────────────────┐
│ 传统 GC 的停顿问题 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 用户请求 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Stop The World │ │ │
│ │ │ │ │ │
│ │ │ 1 秒 ────────────────────────────────► │ │ │
│ │ │ │ │ │
│ │ │ 用户体验:卡顿 1 秒 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 响应返回 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 传统 GC 停顿时间:100ms ~ 1000ms │
│ 现代应用需求:< 10ms │
│ │
└─────────────────────────────────────────────────────────────┘传统优化的极限
| 收集器 | 停顿时间 | 原理 |
|---|---|---|
| Serial | 很长 | 单线程 |
| Parallel | 中 | 多线程 |
| CMS | 短(并发) | 并发标记 |
| G1 | 可控 | Region 选择 |
| 理论极限 | ~10ms | 传统设计无法突破 |
ZGC:停顿时间 < 1ms
ZGC 的核心设计
┌─────────────────────────────────────────────────────────────┐
│ ZGC:着色指针(Colored Pointers)技术 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 64 位系统,Java 对象引用使用 48-56 位 │
│ │ │
│ ├── 42 位:堆地址(最大 4TB) │
│ ├── 4 位:ZGC 标记状态(Marked0, Marked1, Remapped) │
│ └── 18 位:保留 │
│ │
│ 关键洞察: │
│ - 标记状态存在指针本身,而不是对象头 │
│ - 读取对象时,通过指针位运算判断状态 │
│ - 永不修改对象头,只修改指针 │
│ │
└─────────────────────────────────────────────────────────────┘ZGC 的三色标记 + 读屏障
java
// ZGC 的读屏障(Load Barrier)
public Object loadObject(Object obj) {
Object o = obj.field;
// 读屏障:检查指针的 Marked 状态
// 如果对象被移动,自动修正引用(自愈)
if (needsBarrier(o)) {
return sanitize(o); // 引用修正
}
return o;
}ZGC 的并发阶段
┌─────────────────────────────────────────────────────────────────────┐
│ ZGC 收集周期 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 并发标记 │───►│ 并发重定位 │───►│ 并发预清理 │ │
│ │ Concurrent │ │ Concurrent │ │ Concurrent │ │
│ │ Mark │ │ Relocate │ │ Prepare │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 初始标记 │ │ 最终重定位 │ │ 重新标记 │ │
│ │ (STW)很短 │ │ (STW)很短 │ │ (STW)很短 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 所有并发阶段:用户线程和 GC 线程同时运行 │
│ STW 时间:< 1ms │
│ │
└─────────────────────────────────────────────────────────────────────┘ZGC 的引用修正(自愈)
┌─────────────────────────────────────────────────────────────┐
│ ZGC 的自愈机制 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 对象移动时: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 旧地址 ──移动──► 新地址 │ │
│ │ │ │ │
│ │ │ 对象 A 引用旧地址 │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ 读取 A 时触发读屏障 │ │ │
│ │ │ 检测到 A 的地址 是旧地址(Remapped) │ │ │
│ │ │ 自动修正引用到新地址 │ │ │
│ │ │ 返回新地址 │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 多次读取后,所有引用都「自愈」到新地址 │
│ │
└─────────────────────────────────────────────────────────────┘Shenandoah:Oracle 的开源方案
与 ZGC 的区别
┌─────────────────────────────────────────────────────────────┐
│ ZGC vs Shenandoah 核心对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 特性 ZGC Shenandoah │
│ ───────────────────────────────────────────────────── │
│ 染色指针 ✓(硬件支持) ✓(软件模拟) │
│ 支持版本 JDK 11+ JDK 12+ │
│ 维护方 Oracle Red Hat │
│ 分代支持 ZGC 15+ 支持 无(纯不分代) │
│ 堆上限 16TB 无限制 │
│ 并发压缩 ✓ ✓ │
│ GC 线程数 自动 可配置 │
│ │
│ 共同点: │
│ - 都是亚毫秒级停顿 │
│ - 都是并发收集 │
│ - 都不分代(Shenandoah)或可选分代(ZGC 15+) │
│ │
└─────────────────────────────────────────────────────────────┘Shenandoah 的 Brooks Pointer
java
// Shenandoah 使用转发指针(Brooks Pointer)
// 对象头中增加一个指针,指向对象的新位置
public class BrooksPointer {
// 对象头结构
// [Brooks Pointer] [Mark Word] [Class Pointer] [Fields]
// ↑ ↑
// 转发指针 指向当前对象位置
// 读取对象时,检查转发指针
public Object load(Object obj) {
// 检查 Brooks Pointer 是否指向新位置
if (obj.brooksPointer != obj) {
// 对象已移动,更新引用
obj = obj.brooksPointer;
}
return obj;
}
}性能对比
停顿时间对比
| 收集器 | Young GC | Mixed GC | Full GC |
|---|---|---|---|
| Serial | 500ms | - | 1000ms |
| Parallel | 200ms | - | 500ms |
| CMS | 50ms | - | 300ms |
| G1 | 20ms | 50ms | 100ms |
| Shenandoah | < 1ms | < 1ms | < 1ms |
| ZGC | < 1ms | < 1ms | < 1ms |
吞吐量对比
┌─────────────────────────────────────────────────────────────┐
│ 吞吐量 vs 停顿时间 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 吞吐量 │
│ ▲ │
│ │ ╭─ ZGC/Shenandoah │
│ │ ╭────╯ │
│ │ ╭────╯ ╭─ G1 │
│ │ ╭────╯ ╭───╯ ╭─ Parallel │
│ │ ╭────╯ ╭───╯ ╭───╯ ╭─ Serial │
│ │ ╭────╯ ╭───╯ ╭───╯ ╭───╯ │
│ └──────────────────────────────────────► │
│ 停顿时间 │
│ ◄── ───────► │
│ 短 长 │
│ │
└─────────────────────────────────────────────────────────────┘配置与使用
ZGC 配置
bash
# JDK 11+ 启用 ZGC
java -XX:+UseZGC your.Application
# JDK 15+ 分代 ZGC(推荐)
java -XX:+UseZGC -XX:+ZGenerational your.Application
# 配置参数
-XX:MaxGCPauseMillis=1 # 停顿时间目标(软目标)
-XX:ConcGCThreads=4 # 并发 GC 线程数
-Xmx32g -Xms32g # ZGC 支持大堆
# JDK 21+ 推荐配置
java -XX:+UseZGC \
-XX:+ZGenerational \
-XX:MaxGCPauseMillis=1 \
-Xmx64g -Xms64g \
your.ApplicationShenandoah 配置
bash
# JDK 12+ 启用 Shenandoah
java -XX:+UseShenandoahGC your.Application
# 配置参数
-XX:ShenandoahGCHeuristics=adaptive # 自适应策略
-XX:ConcGCThreads=4 # 并发 GC 线程数
-XX:MaxGCPauseMillis=1 # 停顿时间目标适用场景
ZGC / Shenandoah 的最佳场景
| 场景 | 原因 |
|---|---|
| 低延迟敏感应用 | 金融交易、实时系统、游戏服务器 |
| 大堆应用 | 支持 16TB+ 堆,GC 停顿仍 < 1ms |
| 容器环境 | 内存资源动态变化,ZGC 适应性好 |
| JDK 11+ 新项目 | 开箱即用的低延迟方案 |
不适合的场景
| 场景 | 原因 |
|---|---|
| 小堆应用 | ZGC 的开销(读屏障、染色指针)在小堆下不划算 |
| 极高吞吐量需求 | ZGC 吞吐量比 Parallel 低约 10-15% |
| JDK 8 项目 | 需要升级 JDK |
面试追问方向
- ZGC 的染色指针(Colored Pointers)是什么?为什么需要它?
- ZGC 和 Shenandoah 都声称 < 1ms 停顿,它们是怎么做到的?
- ZGC 的「自愈」机制是什么?为什么需要读屏障?
- ZGC 为什么支持 16TB 堆?而其他收集器支持不了这么大?
- ZGC 和 G1 在设计理念上有什么本质区别?
- JDK 15 的分代 ZGC(ZGenerational)和原来的 ZGC 有什么区别?
- ZGC 的吞吐量为什么比其他收集器低?牺牲了什么换取了低延迟?
