ZGC 深度解析
理解ZGC如何实现亚毫秒级停顿、并发类卸载与混合堆压缩,以及在超大规模堆场景下的生产调优实践。
1. 核心设计理念
1.1 ZGC vs 传统GC的根本区别
传统GC(如G1、CMS)在标记和压缩阶段需要"Stop-The-World"(STW),暂停所有应用线程。对于TB级堆内存,这种暂停可能长达数秒。ZGC的设计目标是将STW时间控制在亚毫秒级别,无论堆大小如何。
1.2 着色指针与读屏障
ZGC的核心创新是着色指针(Colored Pointers)**和**读屏障(Load Barrier)**。与传统GC使用对象头中的mark word不同,ZGC在指针本身上存储元数据。
// ZGC 使用 64 位指针的高位存储元数据
// 42位用于地址 (对应 4TB 地址空间)
// 4位用于视图 (View,用于分代ZGC)
// 17位用于GC状态位
Bit 63 48 0
// [0000 0000 0000 0000] [xxxx xxxx xxxx xxxx]
// └─ 42位对象地址
// 着色位定义 (ZGC使用了4个颜色位)
final long ZGCCardColorMask = (1L << 0); // Remapped
final long ZGCMarkColorMask1 = (1L << 1); // Marked0
final long ZGCMarkColorMask2 = (1L << 2); // Marked1
final long ZGCFinalizableMask = (1L << 3); // Finalizable
读屏障的开销极低——只有约4%的吞吐量损耗。相比之下,传统GC的全量STW可能造成50%以上的停顿时间。对于延迟敏感型应用,这是完全值得的trade-off。
1.3 并发执行的关键阶段
ZGC将GC过程分解为多个并发阶段,每个阶段都不需要长时间持有锁或暂停应用:
// ZGC 周期中的主要阶段
Phase 描述 并发 STW
─────────────────────────────────────────────────
Pause Mark 初始标记根集合 ❌ ✅ ~0.1ms
Concurrent 标记存活对象 ✅ ❌
Pause Reloc 重定位根集合 ❌ ✅ ~0.1ms
Concurrent 重定位存活对象 ✅ ❌
Concurrent 更新引用 ✅ ❌
Concurrent 销毁空页面 ✅ ❌
2. 核心技术机制
2.1 着色指针如何实现并发
当GC线程并发修改对象的引用关系时,指针的颜色位也随之更新。应用线程在读取对象引用时,读屏障会检查颜色位——如果对象已被移动,读屏障会自动更新引用为新地址。
// ZGC 读屏障伪代码
Object loadObject(Object obj) {
Object raw = obj;
// 读屏障检查指针颜色
long color = raw & ZGCMarkColorMask;
if (color == Marked0 || color == Marked1) {
// 对象已被标记,引用有效
return raw;
}
if (color == Remapped) {
// 对象在移动过程中,需要更新引用
Object newAddr = followForwardingPointer(raw);
return newAddr;
}
// 其他情况...
return raw;
}
2.2 自愈引用机制
ZGC的"自愈"(Self-Healing)机制是其低延迟的关键。当应用线程通过读屏障发现对象已被移动时,它会更新本地引用为新地址。这意味着下一次访问同一对象时,无需再次经过读屏障。
2.3 页面系统与并行重定位
ZGC将堆划分为多个页面(Page),每个页面大小可以是2MB、32MB或N×2MB。GC并发地将存活对象从一个页面迁移到另一个页面,而不是对整个堆进行紧凑压缩。
// ZGC 页面类型
enum ZPageType {
Small (2MB), // 对象 < 256KB
Medium(32MB), // 对象 256KB ~ 1MB
Large (N×2MB), // 对象 > 1MB,单独分配
}
// 重定位集(Relocation Set)
// ZGC 选择低密度页面进行重定位,优先回收空间
ZRelocationSet = {
pages: [ZPage#12, ZPage#45, ZPage#78],
selectPages(): // 选择标准:存活对象比例 < 阈值
}
ZGC对超大对象(>1MB)的处理有额外开销。大对象使用N×2MB的Large页面,重定位时需要更新所有引用。如果应用有大量超大对象(如消息队列的批量消息),需要评估是否适合ZGC。
3. 分代ZGC(ZGC Gen)
3.1 为什么需要分代
原生ZGC是非分代的,意味着所有对象都在同一片堆中管理。这导致"朝生夕灭"的对象也需要参与完整的GC周期,造成不必要的扫描开销。JDK 21引入的ZGC Gen(ZGenerational)解决了这个问题。
// 分代ZGC将堆分为两个区域
// 年轻代 (Young Generation)
// ├── Eden: 新对象分配
// └── Survivor: 存活但未晋升的对象
// 老年代 (Old Generation)
// └── 老对象: 长期存活的对象
// 年轻代GC(Minor GC)
// - 只扫描年轻代,停顿更短
// - 使用视图位(View)区分年轻代/老年代对象
// 老年代GC(Major GC)
// - 扫描全部堆
// - 仍保持亚毫秒级停顿
3.2 视图位机制
分代ZGC利用着色指针的额外位来区分对象所属代际:
// ZGC Gen 使用 2 个视图位
// View = 00: 32位地址模式(未使用)
// View = 01: 年轻代视图
// View = 10: 老年代视图
// View = 11: 通用视图(未分代ZGC使用)
final long ZGenViewMask = (3L << 42); // 42-43位
// 年轻代对象的指针格式
// [View:2][0000...][Color:4][Offset:58]
// View = 01 表示年轻代
// 老年代对象的指针格式
// [View:2][0000...][Color:4][Offset:58]
// View = 10 表示老年代
4. 生产环境调优
4.1 JVM参数配置
ZGC的参数设计极为精简,通常只需要设置堆大小即可运行。但对于生产环境,还需要考虑一些关键参数:
# 基础配置 - 启用ZGC
-XX:+UseZGC
# 堆大小配置(根据应用需求)
-Xms8g -Xmx8g
# 分代ZGC(推荐,JDK 21+)
-XX:+UseZGC -XX:+ZGenerational
# GC线程自适应(默认开启)
-XX:+UseDynamicGCThreads
# 显式设置GC线程数(可选)
-XX:ZGCThreads=32
# 调高GC触发阈值(适合高吞吐量场景)
# 默认70%,提高到85%可减少GC频率
-XX:ZCollectionInterval=60 # 最大GC间隔(秒)
-XX:zAllocationStall=10 # 分配阻塞阈值(秒)
# 亲和性绑定(低延迟敏感场景)
-XX:+BindGCThreadsTocpus # 绑定GC线程到CPU
-XX:+UseNUMA # 启用NUMA感知
4.2 监控与诊断
ZGC提供了丰富的日志和监控指标:
# 详细GC日志
-Xlog:gc*,gc+phases=debug,gc+heap=debug:file=zgc.log:time:
# ZGC特定指标(JDK 11+)
# 关键指标:
# - ZGC Cycles: GC周期总数
# - ZGC Pauses: 停顿次数和总时间
# - ZGC Allocation Rate: 分配速率
# - ZGC Heap Used: 堆使用量
# JMX监控(通过jconsole或jvisualvm)
com.sun.management.GarbageCollectorMXBean
ZGC.getCollectionCount()
ZGC.getCollectionTime()
# 使用jdk.jcmd工具诊断
jcmd <pid> VM.log what="zgc"
4.3 性能对比
在典型延迟敏感型负载下,ZGC与G1GC的对比如下:
| 指标 | G1GC | ZGC (非分代) | ZGC (分代) |
|---|---|---|---|
| 最大停顿时间 | 200-500ms | <1ms | <1ms |
| 平均停顿时间 | 30-100ms | <0.5ms | <0.2ms |
| 吞吐量损失 | ~5% | ~4% | ~3% |
| 内存开销 | 低 | 低 (~1%) | 中等 (~5%) |
| 适合堆大小 | <64GB | 任意大小 | 任意大小 |
5. 适用场景与局限
✅ ZGC 最佳场景
- 低延迟服务(交易系统、游戏服务器)
- 大内存堆(>64GB)应用
- 需要可预测停顿的应用
- JDK 11+ 生产环境
❌ ZGC 不适合场景
- 极致的吞吐量追求(考虑Shenandoah)
- 大量超大对象(>1MB)
- JDK 8 或更早版本
- 极端内存约束环境
ZGC与Shenandoah的选择:两者都是低延迟GC,但ZGC需要更少内存开销,Shenandoah与JDK 8兼容更好。对于JDK 11+,ZGC是首选。如果仍在JDK 8,Shenandoah是唯一低延迟选项。