📅 2026-05-22 👤 张伟 🏷️ Java 21 · GC · 低延迟

ZGC 深度解析

理解ZGC如何实现亚毫秒级停顿、并发类卸载与混合堆压缩,以及在超大规模堆场景下的生产调优实践。

1. 核心设计理念

1.1 ZGC vs 传统GC的根本区别

传统GC(如G1、CMS)在标记和压缩阶段需要"Stop-The-World"(STW),暂停所有应用线程。对于TB级堆内存,这种暂停可能长达数秒。ZGC的设计目标是将STW时间控制在亚毫秒级别,无论堆大小如何。

STW 标记
并发标记
并发重定位


16TB 堆内存
<1ms 停顿

1.2 着色指针与读屏障

ZGC的核心创新是着色指针(Colored Pointers)**和**读屏障(Load Barrier)**。与传统GC使用对象头中的mark word不同,ZGC在指针本身上存储元数据。

ZGC 着色指针结构 (64位)
// 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 并发阶段示意
// 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 页面类型
// 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 内存布局
// 分代ZGC将堆分为两个区域
//  年轻代 (Young Generation)
//  ├── Eden: 新对象分配
//  └── Survivor: 存活但未晋升的对象
//  老年代 (Old Generation)
//  └── 老对象: 长期存活的对象

// 年轻代GC(Minor GC)
// - 只扫描年轻代,停顿更短
// - 使用视图位(View)区分年轻代/老年代对象

// 老年代GC(Major GC)
// - 扫描全部堆
// - 仍保持亚毫秒级停顿

3.2 视图位机制

分代ZGC利用着色指针的额外位来区分对象所属代际:

分代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 推荐生产配置
# 基础配置 - 启用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提供了丰富的日志和监控指标:

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是唯一低延迟选项。