摘要ZGCZ Garbage Collector是 JDK 11 引入的革命性垃圾收集器以亚毫秒级停顿为目标通过**染色指针Colored Pointers和读屏障Load Barrier**两项核心创新将几乎所有 GC 工作包括对象整理/移动都变为与用户线程并发执行将 STW 停顿时间压缩至 1~10ms 以内与堆大小无关。本文深入解析 ZGC 的技术原理64位指针的元数据染色方案、并发整理的实现机制转发表读屏障自愈、ZGC 的 GC 周期各阶段以及 JDK 15 分代 ZGC 的改进。附完整的 ZGC 配置参数与调优实践适合对延迟极度敏感的在线服务场景。引言当 G1 的 200ms 停顿目标对你来说还是太长当你面对的是在线游戏、实时交易、流式处理等对 GC 停顿极度敏感的场景普通的低延迟调优已经触及天花板。ZGC 的出现改变了一切。JVM GC 停顿时间演进 Serial GC → 秒级停顿 Parallel GC → 几百ms CMS → 几十到几百ms G1 → 几十到几百ms可设200ms目标 ZGC → 1~10ms与堆大小无关这个与堆大小无关是划时代的——哪怕是 1TB 的堆ZGC 的停顿时间依然控制在毫秒级。这在以往是不可想象的。一、ZGC 的核心创新染色指针1.1 传统 GC 的限制传统 GC包括 G1在对象整理移动对象时必须 Stop-The-World因为移动对象到新地址更新所有指向该对象的引用步骤1和2必须是原子的否则用户线程会拿到悬空指针ZGC 的革命通过染色指针和读屏障将对象移动和引用更新变为与用户线程并发的操作。1.2 染色指针Colored Pointers在 64 位 JVM 中理论上指针有 64 位但实际上只用了低 42 位来寻址因为 Linux 的虚拟地址空间通常为 128TB还有 22 位是浪费的。ZGC 从这剩余的位中借用4 个 bit来存储 GC 状态信息64 位对象引用ZGC bit 63 42 41 18 17 16 15 14 13 0 ┌────────────┬─────────┬──────┬──────┬──────────────────────────┐ │ 保留不用 │ 对象地址 │Remap │Mark1 │ ... │ │ (22 bits) │ (42 bits)│ (1b) │ (1b) │ │ └────────────┴─────────┴──────┴──────┴──────────────────────────┘ 不使用 实际地址 ↑ ↑ Remap Mark 4 个染色 bit 的含义ZGC 各版本略有不同 Finalizable (1bit)对象是否只能通过 finalize() 访问 Remapped (1bit)对象是否已迁移到新地址 Marked1 (1bit)标记阶段使用奇数 GC 周期 Marked0 (1bit)标记阶段使用偶数 GC 周期染色指针的意义传统 GC 对象移动后需要遍历所有引用并修改指针值指向新地址 这个过程必须 STW ZGC 染色指针 对象移动后旧引用的 Remapped bit 仍为0表示引用还未更新 当用户线程访问这个旧引用时读屏障检测到 Remapped0 读屏障自动将指针更新到新地址并设置 Remapped1 效果不需要一次性更新所有引用引用懒更新 用户线程访问到哪里就更新到哪里无需 STW⚠️染色指针的限制只能在 64 位 JVM 上使用32 位指针没有足够的空闲 bit目前只支持 Linux/x86-64、Linux/AArch64、macOSApple Silicon堆大小上限JDK 11 为 4TBJDK 13 为 16TB1.3 多重映射Multi-Map技术为了让染色指针的地址语义正确工作ZGC 使用了 Linux 的内存映射技术将同一块物理内存映射到多个虚拟地址段物理内存堆 [对象A][对象B][对象C]...物理地址 0x0001~0x00FF 虚拟地址映射 Marked0 视图0x0001_0000 ~ 0x00FF_0000bit 42 0, bit 4 0 Marked1 视图0x0001_0010 ~ 0x00FF_0010bit 42 0, bit 4 1 Remapped 视图0x0001_0100 ~ 0x00FF_0100bit 42 1, bit 4 0 三个视图指向同一块物理内存 访问任何一个视图地址操作的都是同一个对象。 好处 修改染色 bit 只是改变了使用哪个虚拟地址段访问对象 操作系统层面依然是合法的内存访问 无需真正复制指针所指向的数据二、读屏障Load Barrier2.1 什么是读屏障读屏障是 ZGC 在每次从堆中读取对象引用时插入的一小段检测逻辑// 普通 Java 代码ObjectobjsomeField;// 从堆中读取引用// ZGC 读屏障在 JIT 编译后变为简化示意ObjectobjsomeField;if(obj!null!is_good_color(obj)){objslow_path_barrier(obj);// 慢路径修复引用}对比写屏障CMS/G1 使用写屏障在引用赋值时触发obj.field newRef读屏障在引用读取时触发obj someField读屏障的优势读操作比写操作频繁但读屏障检测逻辑极简几条机器指令对性能影响可控通常 3%~5%。2.2 读屏障如何实现并发整理ZGC 并发整理的核心工作流程阶段1并发移动对象Concurrent Relocate ┌──────────────────────────────────────────────┐ │ GC 线程将 Region 中的存活对象复制到新 Region │ │ 在转发表Forwarding Table中记录 │ │ 旧地址 → 新地址 │ └──────────────────────────────────────────────┘ 此时部分引用指向的还是旧地址 阶段2用户线程访问旧引用读屏障触发 用户线程: Object obj field; // 读取引用指向旧地址 obj.doSomething(); // 使用 obj 读屏障检测这个引用的 Remapped bit 0未更新 ↓ 慢路径查转发表找到新地址 ↓ 更新引用field new_address顺便修复持有这个引用的字段 设置 Remapped bit 1 ↓ obj 现在指向新地址用户线程正常访问 ✅ 结果 对象移动后引用通过读屏障懒更新 无需 STW 批量更新所有引用 用户线程在访问时顺便完成了引用修复2.3 转发表Forwarding Table转发表是 ZGC 在并发整理期间维护的映射表转发表存储在 Region Header 中 旧地址 → 新地址 0x7f0001234000 → 0x7f0009876000 0x7f0001234010 → 0x7f0009876020 0x7f0001234020 → 0x7f0009876040 ... 当所有指向旧 Region 的引用都被更新后通过读屏障修复 旧 Region 可以被释放转发表也可以删除。三、ZGC 的 GC 周期3.1 完整的 ZGC 周期ZGC GC 周期各阶段 用户线程: ████│ │██████████████│ │████████████████████│ │████ GC线程: │1 │ 2 并发标记 │3 │ 4 并发引用处理 │5 │ │ │ │ │ 5 并发重定位准备 │ │ │ │ │ │ 6 并发重定位 │ │ └──┘ └──┘ └──┘ STW STW STW 初始标记 重新标记 初始化重定位 (1~2ms) (1~2ms) (1~2ms) 阶段1初始标记Pause Mark Start— STW极短1~2ms - 标记 GC Roots 直接可达的对象不遍历引用链 - 设置 Marked0 或 Marked1交替使用 阶段2并发标记Concurrent Mark— 并发最耗时 - 从 GC Roots 出发遍历对象图 - 使用读屏障记录并发期间的引用变化 - 标记期间新分配的对象自动视为存活 阶段3重新标记Pause Mark End— STW极短1~2ms - 处理并发标记期间产生的少量引用变化SATB缓冲区 - 弱引用处理软引用、弱引用、虚引用、Finalizer 处理 阶段4并发引用处理Concurrent Reference Processing— 并发 - 处理各种引用对象SoftReference、WeakReference 等 阶段5并发重定位准备Concurrent Prepare for Relocation— 并发 - 选择垃圾最多的 Region 组成 Relocation Set - 为每个待移动 Region 初始化转发表 阶段6初始化重定位Pause Relocate Start— STW极短 - 扫描 GC Roots更新其中指向 Relocation Set 的引用 阶段7并发重定位Concurrent Relocate— 并发 - GC 线程将存活对象从 Relocation Set 复制到新 Region - 用户线程的读屏障也在同步修复引用 注ZGC 没有单独的重新映射阶段 引用修复Remapping被合并进下一次 GC 周期的并发标记中3.2 ZGC 的停顿时间为什么这么短ZGC 的三次 STW 各有多长 1. Pause Mark Start初始标记1~2ms - 只扫描 GC Roots不遍历引用链 - 时间与堆大小无关只与 GC Roots 数量有关 2. Pause Mark End重新标记1~2ms - SATB 缓冲区通常很小并发标记期间积累的 - 有超时机制如果超过 1ms 就放弃留给下次处理 3. Pause Relocate Start初始化重定位1~2ms - 只扫描 GC Roots与堆大小无关 总停顿 ≈ 3~6ms与堆大小无关四、分代 ZGCJDK 214.1 为什么需要分代 ZGC早期 ZGCJDK 11~20是无分代设计每次 GC 扫描整个堆。这有一个问题无分代 ZGC 的缺陷 - 每次 GC 都扫描整个堆包括长期存活对象 - 短命对象通常占大多数和长期存活对象被同等对待 - CPU 消耗较大GC 线程抢占用户线程的 CPU 资源 - 对吞吐量有一定影响 分代假说弱分代假说依然成立 大多数对象都是短命的 针对年轻代做高频 GC远比全堆 GC 更高效4.2 分代 ZGC 的架构JDK 21 正式引入分代 ZGC-XX:ZGenerationalJDK 23 成为默认分代 ZGC 堆布局 ┌──────────────────────────────────────────────────────────────┐ │ ZGC 堆仍是 Region 化 │ │ │ │ ┌──────────────────────────────────┐ │ │ │ 年轻代区域 │ ← 高频 GC毫秒级 │ │ │ Small Region (256KB) │ │ │ │ Medium Region (32MB) │ │ │ └──────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────┐ │ │ │ 老年代区域 │ ← 低频 GC │ │ │ Large Region (多 Region 组成) │ │ │ └──────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ 分代 ZGC 的 GC 模式 Minor GCYoung GC只 GC 年轻代频率高停顿 1ms Major GCOld GCGC 老年代频率低 Full GC退化情况应避免分代 ZGC vs 无分代 ZGC指标无分代 ZGCJDK 11~20分代 ZGCJDK 21GC 吞吐量较低整堆扫描更高年轻代高频回收停顿时间1~10ms1ms年轻代GC内存占用较低无额外分代结构稍高跨代引用追踪适用场景大堆低延迟兼顾吞吐量与低延迟五、ZGC 完整调优参数5.1 启用与基础配置# 启用 ZGC # JDK 11~14实验性-XX:UnlockExperimentalVMOptions-XX:UseZGC# JDK 15正式 GA-XX:UseZGC# JDK 21 分代 ZGC推荐-XX:UseZGC-XX:ZGenerational# 堆大小 -Xms16g-Xmx16g# 建议初始最大避免堆扩容引起的停顿# 停顿时间目标ZGC 几乎不需要设置默认已极低# ZGC 不支持 -XX:MaxGCPauseMillis与 G1 不同# 内存预留避免并发失败-XX:ZAllocationSpikeTolerance2.0# 分配速率波动容忍度默认2.05.2 GC 线程配置-XX:ConcGCThreads4# 并发 GC 线程数默认自动计算# 设置过小GC 跟不上分配速率可能导致堆撑满# 设置过大抢占过多 CPU影响业务吞吐量-XX:ParallelGCThreads8# STW 阶段并行线程数默认CPU核数不常需要调整5.3 Soft Max Heap Size重要特性# SoftMaxHeapSize软性堆上限ZGC 独有-XX:SoftMaxHeapSize12g# 与 -Xmx16g 配合使用# 作用# ZGC 在正常情况下将堆使用控制在 SoftMaxHeapSize 以内# 当内存压力大时允许临时使用到 Xmx# 类似弹性内存平时节约内存压力大时自动扩展# 典型场景# 容器部署Xmx容器内存上限SoftMaxHeapSize平时预留内存# 可以避免因为偶发流量高峰被 OOM Kill5.4 日志配置# JDK 9 统一日志-Xlog:gc*:file/var/log/jvm/gc.log:time,level,tags:filecount5,filesize20m# ZGC 专项日志-Xlog:gcheapdebug# 堆使用情况-Xlog:gcphasesdebug# GC 各阶段详情-Xlog:safepointdebug# SafePoint 停顿信息六、ZGC vs G1 选型建议何时选择 ZGC而非 G1 ✅ 选择 ZGC 的场景 1. GC 停顿目标 50msG1 的 200ms 无法满足 2. 堆内存 8GB且对延迟敏感ZGC 停顿不随堆大小增长 3. 在线游戏服务器帧率稳定要求不允许 GC 卡顿 4. 实时交易/风控系统P99.9 延迟有硬性要求 5. 流式数据处理低延迟管道不能有长时间停顿 ⚠️ 使用 ZGC 的注意事项 1. 吞吐量比 G1 略低读屏障开销 3~5% 2. JDK 15 才是 GA 版老项目需要升级 JDK 3. 内存占用比 G1 略高转发表 染色指针元数据 4. 分代 ZGC 需要 JDK 21 5. 不支持 -XX:MaxGCPauseMillis不需要自然极短 ✅ 继续使用 G1 的场景 1. 停顿 200ms 以内可以接受 2. 对 CPU 吞吐量敏感批处理混合场景 3. JDK 8/11无法升级到 JDK 15 4. 堆内存 4GBZGC 优势不明显七、生产环境 ZGC 配置示例7.1 高并发在线服务JDK 2132核64GBjava\-XX:UseZGC\-XX:ZGenerational\-Xms32g\-Xmx32g\-XX:SoftMaxHeapSize28g\-XX:ConcGCThreads8\-XX:ParallelGCThreads16\-XX:ZAllocationSpikeTolerance3.0\-XX:HeapDumpOnOutOfMemoryError\-XX:HeapDumpPath/var/log/jvm/\-Xlog:gc*:file/var/log/jvm/gc.log:time,level,tags:filecount10,filesize50m\-jarapp.jar7.2 ZGC 监控关键指标# 实时监控 GC 状态jstat-gcutilpid1000# 关注指标# YGC年轻代 GC 次数分代ZGC# FGCFull GC 次数应该为0# FGCTFull GC 总耗时应该为0# GCT总 GC 时间占比# ZGC 特有监控jstat-gcpid|head# 查看 ZGC 统计八、总结ZGC 代表了 JVM GC 技术的最新高度染色指针在 64 位指针的高位存储 GC 元数据Marked/Remapped使 GC 状态查询成为指针操作而非对象头读取读屏障每次读取堆引用时检测染色状态自动修复过期引用懒更新无需 STW 批量更新引用并发整理通过转发表记录旧→新地址映射GC 线程和用户线程并发进行对象复制和引用修复三次极短 STW每次约 1~2ms与堆大小完全无关分代 ZGCJDK 21引入年轻/老年代分离大幅提升吞吐量同时保持超低停顿ZGC 没有银弹——它以约 3~5% 的吞吐量代价换取极致的低停顿。对于绝大多数在线服务这是完全值得的权衡。下一篇预告除了 ZGC还有另一个超低停顿收集器——Shenandoah。它由 RedHat 贡献采用了不同于 ZGC 的技术路线Brooks 间接指针 并发整理在某些场景下比 ZGC 更有优势。第08篇将带你深入 Shenandoah 的世界并做两者的横向对比。系列导航上一篇【JVM深度解析】第06篇G1垃圾收集器深度解析下一篇【JVM深度解析】第08篇Shenandoah垃圾收集器深度解析系列目录JVM深度解析系列全集参考资料JEP 333: ZGC: A Scalable Low-Latency Garbage Collector (Experimental)JEP 377: ZGC: A Scalable Low-Latency Garbage CollectorJEP 439: Generational ZGCZGC Wiki (OpenJDK)Per Liden: ZGC — Low Latency GC for OpenJDKOracle: ZGC Documentation《深入理解Java虚拟机第3版》第3章 — 周志明著