ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JVM ZGC 垃圾回收器深度解析

JVM ZGC 垃圾回收器深度解析 JVM ZGC 垃圾回收器深度解析ZGCZ Garbage Collector是 Oracle 于 JDK 11 引入实验特性、JDK 15 转正JEP 377、JDK 21 实现分代JEP 439的新一代回收器。它的核心承诺只有一条停顿时间与堆大小、对象存活率完全无关稳定在亚毫秒级1ms——16KB 到 16TB 的堆停顿都是同一个量级。支撑这个承诺的是三项关键技术染色指针Colored Pointers、读屏障Load Barrier、并发整理Concurrent Relocation。本文从底层机制讲到分代演进覆盖面试与实战所需的全貌。一、ZGC 要解决什么问题回顾回收器演进的主线矛盾回收器停顿量级停顿与堆大小关系根本限制CMS10~200ms基本无关碎片、Concurrent Mode FailureG110~200ms弱相关Root 扫描/Remset 仍随堆增长停顿压不过几十毫秒ZGC1ms完全无关吞吐损失读屏障转发表不分代时吞吐偏低CMS 和 G1 的并发标记解决了标记的并发性但对象搬迁整理/复制始终是 STW 的——因为在对象被移动、引用未被修正的中间状态里业务线程会读到悬空的旧地址。这是 G1 停顿压不进 10ms 以内的根本原因Evacuation 必须暂停所有线程一起做。ZGC 的回答是把对象移动本身也做成并发的。业务线程与 GC 线程同时读一个正在被搬运的对象居然不崩、不阻塞、不需要 STW——这靠的是染色指针 读屏障 转发表的组合拳。为什么对象移动必须停顿业务线程访问对象地址的方式① 通过对象引用指针访问 → 搬走后指针失效② 通过对象内部的字段访问this.field→ 对象整体搬走后 this 也失效CMS/G1 的解法是停下所有人搬完、改完所有引用再放行ZGC 的解法是你先搬我每次读引用时自己发现指针过期了顺路修复自愈。二、染色指针Colored Pointers在指针里藏 GC 元数据2.1 设计思路传统 JVM 把 GC 元数据是否可达、是否被标记过存在堆外结构里卡表、位图、RSet。ZGC 反其道而行把标记状态直接编码进 64 位对象指针的空闲高位。JDK 11~14 的 ZGC 指针布局x86-6464 位对象指针x86-64未使用18 bit63~46M11 bit45M01 bit44R1 bit43F1 bit42对象地址42 bit41~0说明M0 Marked0M1 Marked1R RemappedF Finalizable。颜色位共 4 bit位于 42~45 位对象地址占低 42 位41~0可寻址 4TB 堆空间。四个颜色位及其含义位含义Marked0 / Marked1对象在本轮 / 上一轮标记周期中的存活状态两个位交替使用实现两轮标记的无缝衔接无需周期之间清空Remapped该指针已被修正为指向对象搬迁后的新地址重定位完成Finalizable该指针仅被 finalizer 引用即将死亡的缓冲状态不参与正常标记为什么用 Marked0/Marked1 两个交替位ZGC 的标记周期是连续循环的轮到下一轮时上一轮的颜色还有效对象可能还是活的。用两个位交替表示本轮活和上轮活周期切换时无需全局清零任何颜色状态——一次位翻转就完成换代。2.2 多重映射与后续演进面试加分点x86-64 实际只用 48 位虚拟地址。JDK 11~14 的 ZGC 把同一块物理内存映射到虚拟地址空间的三个视图ViewMarked0 视图、Marked1 视图、Remapped 视图。写指针时按颜色落到对应视图读时按视图天然区分颜色——利用了操作系统的虚拟内存重映射机制。JDK 15 起JDK-8225193移除了多重映射改为单一映射 读屏障中直接校验指针颜色好处是降低了虚拟地址空间浪费、让堆上限从 4TB 扩展到 16TB、简化了与外国工具profiler、容器的交互。2.3 染色指针的本质收益元数据内联标记状态跟着指针走无需堆外位图访问零额外开销视图即状态对同一物理内存的多个虚拟视图让颜色成为指针天然的一部分为读屏障自愈提供信息载体读屏障检查的就是这几位颜色对象可以瞬间被判活所有指向它的指针颜色一目了然三、读屏障Load BarrierZGC 的灵魂3.1 为什么是读屏障G1/CMS 用的是写屏障在引用赋值时拦截维护卡表/RSet因为它们要解决的是引用关系变了我该记录谁。ZGC 要解决的是另一个问题对象可能已被搬走我读到的指针可能是过期的——所以拦截点必须在读引用的动作上。每当业务线程执行从堆中加载一个对象引用obj.field中的 field 是引用类型时JIT 生成的代码都会插入一小段屏障// 伪代码 ref obj.field; // 1. 加载引用 if (ref.color ! 预期颜色) { // 2. 检查染色位一次寄存器位测试几条指令 ref slow_path(ref); // 3. 过期 → 进入慢路径 } // 继续使用 ref保证一定是好颜色3.2 自愈Self-healing慢路径是 ZGC 设计最精妙的部分。假设对象 X 已被 GC 线程搬到新地址而obj.field里还存着旧地址旧色业务线程读到旧色指针 → 触发慢路径慢路径查转发表找到 X 的新地址把obj.field原地更新为新地址好色然后返回新地址下一次再有人读obj.field读到的已是好色指针零开销通过这就是自愈坏指针每被读一次就被修复一次修复成本被摊销在访问流中且越热的字段修复得越早。整个系统不需要停下来统一修一遍指针。3.3 读屏障的成本与 JIT 优化每次加载堆内引用多了 ~几条机器指令一次比较 分支预测几乎全命中JIT 会做积极优化同一方法内对同一引用的重复加载不重复插屏障指针已确认为好色时不插屏障见下述 STW 期间的预染色实测整体吞吐损失约 5%~15%与工作负载的指针加载密度相关ZGC 中没有卡表、没有 RSet分代前——这些结构与写屏障的成本被整体省掉了部分对冲了读屏障的开销与 G1 写屏障对比面试高频写屏障在写时记录引用关系变更服务于标记完整性增量更新/SATB读屏障在读时校验指针是否过期服务于对象搬迁的正确性自愈一个解决谁引用了谁的记录问题一个解决对象搬走了引用怎么办的访问问题四、回收周期全程只有两个亚毫秒 STW非分代 ZGC 的单个周期并发搬迁无停顿STW 停顿 ②1ms并发阶段无停顿STW 停顿 ①1ms初始标记Pause Mark Start扫描 GC Roots 预染色并发标记 并发重映射Concurrent Mark Remap遍历对象图 顺路修复过期指针最终标记Pause Mark End重扫根 统计存活率 选搬迁集并发搬迁Concurrent Relocate复制存活对象 登记转发表 读者自愈4.1 初始标记Pause Mark Start—— STW1ms只做一件事扫描 GC Roots线程栈、静态变量、JNI 引用把它们直接指向的对象标记上颜色根集合数量与堆大小无关只与线程数、栈深度相关所以停顿与堆无关结束时顺手把根直接引用的指针全部预染色为好色——业务线程恢复后立刻读这些对象也不会触发慢路径4.2 并发标记 并发重映射Concurrent Mark Remap从根出发并发遍历对象图多线程-XX:ConcGCThreads标记过程压栈不压栈都行——ZGC 用指针颜色而不是堆外位图记录存活天然抗并发遍历到坏色指针时读屏障的慢路径会顺路重映射标记的同时修复过期指针把重映射摊销进标记阶段这是 ZGC 与众不同的地方——G1 的 Remark 是独立 STW 修指针ZGC 借业务线程的手在线修结束后仍然指向已被判定死亡对象的引用不做处理对象死了读不读无所谓4.3 最终标记Pause Mark End—— STW1ms处理并发标记期间根集合的少量变更只扫根不扫堆统计各页面Region/Page的存活率选出搬迁集Relocation Set存活率低、回收收益高的页面集合确定下一轮的标记颜色Marked0 ↔ Marked1 翻转4.4 并发搬迁Concurrent Relocate—— 与业务线程并发这是 ZGC 的魔法时刻也是所有前代回收器必须 STW 的阶段GC 线程逐个把 Relocation Set 中页面的存活对象复制到新页面每搬完一个对象在旧页面所在 ZPage 的**转发表Forwarding Table**里登记旧地址 → 新地址转发表按页组织复用页面头部空间查找是 O(1)不修正任何现存引用——修正工作交给后续业务线程下次读到旧色指针时读屏障查转发表自愈下一轮并发标记Remap遍历时顺路把指针改写为新地址搬空的旧页面直接整体回收归还或复用零碎片注意一个反直觉的设计ZGC 不需要记住集来定位谁引用了被搬的对象。它根本不主动找全引用者而是被动等人来读——读者自愈。回收整理从全局搜捕变成了守株待兔省掉了 RSet 的全部内存与维护成本。五、停顿为什么与堆大小完全无关这是面试必须能自圆其说的一问。ZGC 的两个 STW 阶段只做初始标记扫根集合线程栈 全局变量最终标记再扫一遍根 统计页面存活率页面级别的粗粒度统计不是对象级两者都不遍历堆、不搬运对象、不修指针、不建辅助数据结构。堆从 16GB 扩到 1TB变化的只是并发阶段的工作量——而并发阶段不产生停顿。这就是停顿与堆无关的严格证明。对比 G1G1 的 Mixed GC 停顿虽可控但 Root 扫描和 CSet 转移仍随堆/Region 数缓慢增长且停顿预算靠缩小回收范围兑现压到 ~50ms 以下就开始牺牲吞吐。六、分代 ZGCJDK 21JEP 4396.1 为什么 ZGC 也要分代分代假设大多数对象朝生夕死是经验真理但 JDK 21 之前的 ZGC只用单一分代——每个周期都并发遍历全堆大量短命对象也被反复标记、反复搬迁吞吐明显低于 G1。实测常见 10%~20% 的吞吐劣势让很多大吞吐场景不敢上 ZGC。分代 ZGC 用一套极轻的机制补上分代同时保住了核心承诺停顿仍 1ms堆分为 Young GenEden Survivor与 Old GenYoung GC 只遍历新生代根 老年代→新生代的引用老年代 GC 逻辑与非分代 ZGC 基本相同且可以与 Young GC 并发进行互相不阻塞6.2 不用写屏障用读屏障实现记忆集传统分代需要写屏障维护卡表/RSet 来记录老年代谁引用了新生代。分代 ZGC 的做法面试高价值差异点老年代对象引用新生代对象时不记录Young GC 把这些老年代持有者当作根的一部分处理关键技巧读屏障兜底。若业务线程读到老年代 → 新生代的引用且新生代 GC 正在进行/已完成读屏障自愈时顺路处理配合新生代对象地址域与老年代分开的染色约定Young GC 的根集合可以精确定位不需要全堆扫换来的代价分代 ZGC 的读屏障比非分代略重多判断一层代际关系但仍然没有写屏障、没有卡表、没有 RSet——这是与 G1 分代实现的根本区别6.3 效果官方与社区基准分代 ZGC 在保留亚毫秒停顿的同时吞吐达到与 G1 相当±5%的水平。从 JDK 21 开始-XX:UseZGC默认开启分代-XX:ZGenerationalJDK 23 起非分代 ZGC 被移除JEP 490。七、参数速查参数默认值说明-XX:UseZGC—启用JDK 21 默认分代-XX:MaxHeapSize/-Xmx—ZGC 需要更多堆余量并发搬迁期间新旧对象并存 转发表 峰值流量缓冲。经验比 G1 方案多给 20%~50%-XX:SoftMaxHeapSize 0.75×Xmx软上限超过此值 ZGC 会更激进地启动周期但不 OOM。ZGC 最有价值的调优参数之一-XX:ZAllocationSpikeTolerance2分配速率容忍系数。应用分配波动大批处理/大促时调大避免分配尖峰打爆堆-XX:ZCollectionInterval0定时强制启动周期秒。固定节奏回收用于对齐低峰期-XX:ZFragmentationLimit25页面碎片率超过此值才纳入搬迁候选-XX:ConcGCThreads动态并发线程数自适应JDK 17 由 JVM 按负载调节-XX:ZProactivetrue主动 GCCPU 空闲时提前启动周期吞吐换余量一般保留默认-XX:UseDynamicNumberOfGCThreads—自适应并发线程数八、实战要点8.1 什么时候选 ZGC延迟优先P99/P999 停顿要求 10ms交易、撮合、实时风控、游戏服务端大堆32GB 以上数十 GB 到 TB 级G1 停顿开始抬头可用内存充裕吞吐换延迟的第一原则ZGC 吃堆8.2 什么时候不选堆小4G开销占比过高G1/Parallel 更合适吞吐绝对优先的离线批处理Parallel 仍然是王者JDK 8/11/17 运行时限制ZGC 需 JDK 15 才转正、21 才有分代版本8.3 典型部署64G 堆撮合类服务P999 5msjava -Xms64g -Xmx64g -XX:SoftMaxHeapSize48g \ -XX:UseZGC \ -XX:ZAllocationSpikeTolerance3 \ -XX:ZUncommit \ -Xlog:gc*:filegc.log:time,uptime,tags:filecount10,filesize100m8.4 排障信号现象含义动作Allocation Stall日志Allocation Stall分配等待——堆余量不足或分配速率超预期提高 Xmx / SoftMax调大 ZAllocationSpikeTolerance排查分配洪峰来源高频 GC 但回收量小存活对象多、分配速率高分代 ZGC 已大幅缓解检查是否缓存类泄漏吞吐明显低于 G1读屏障 无分代旧版升 JDK 21 用分代 ZGC下面是 ZGC 故障排查决策树从两个典型信号出发逐步定位根因不足充足是否是否否是出现哪种排障信号Allocation Stall日志关键字: Allocation Stall高频 GC 但回收量小日志关键字: GC(.*) Pause检查堆余量命令: jstat -gcutil / jcmd GC.heap_info堆余量是否充足提高 Xmx / SoftMaxHeapSize参数: -XX:SoftMaxHeapSize分析分配速率命令: jstat -gc / jcmd GC.class_histogram分配速率是否超预期调大 ZAllocationSpikeTolerance参数: -XX:ZAllocationSpikeTolerance排查分配洪峰来源工具: async-profiler / JFR排查存活对象命令: jmap -histo:live / jcmd GC.class_histogram是否存在缓存类泄漏定位并修复缓存泄漏工具: JFR / heap dump 分析JDK 版本是否 ≥ 21升级 JDK 21 启用分代 ZGC参数: -XX:UseZGC默认分代检查读屏障开销与并发线程参数: -XX:ConcGCThreads8.5 与容器/监控的兼容性染色指针要求保留高位地址位早期JDK 11~14与某些观测工具/内存映射库冲突JDK 15 移除多重映射后兼容性良好。容器内需注意ConcGCThreads与 CPU limit 的配合避免并发线程抢占业务配额。九、ZGC vs G1 vs Shenandoah维度G1ZGCShenandoah停顿目标10~200ms1ms10ms宣称亚毫秒实际约几 ms停顿与堆关系弱相关完全无关基本无关核心机制SATB RSet 写屏障染色指针 读屏障Brooks 转发指针 读写屏障后来也演进为载荷屏障并发整理✗Evacuation STW✓✓额外内存开销RSet堆的 1%~20%转发表 堆余量隐性每对象多 1 个转发指针字支持方OracleOracleRed HatOpenJDK 社区非 Oracle JDK 默认构建分代是设计即分代JDK 21 起JDK 21 起实验性适用堆4~32G8G~16T8G~数百 GShenandoah 与 ZGC 目标相同、路径不同ZGC 在指针上做文章颜色 转发表Shenandoah 在对象上做文章每个对象头部/外部挂一个永远指向自身的转发指针读写屏障都检查它。ZGC 的方案零对象级元数据、自愈更优雅但强依赖平台指针布局Shenandoah 更可移植但每对象多一个字。十、面试高频问答Q1ZGC 停顿为什么能低于 1msSTW 阶段只做根扫描和页面统计根集合大小与堆无关页面统计是 Region 级粗粒度计数标记、搬迁、指针修正全部并发且大量修正由业务线程的读屏障顺路完成。停顿只与线程数/栈深度相关与堆大小、对象数、存活率无关。Q2读屏障和写屏障的区别拦截点不同、目的不同写屏障在引用赋值时触发维护引用关系元数据卡表/RSet/SATB 队列服务于标记完整性读屏障在加载引用时触发校验指针颜色服务于对象搬迁的正确性实现自愈。ZGC 用读屏障后完全不需要写屏障和堆外记录结构。Q3并发搬迁时业务线程访问被搬对象会怎样分三种① 访问的是旧指针且已过期 → 读屏障慢路径查转发表取新地址并原地修复② 对象还没被搬 → 正常访问下次再查③ 对象正在被搬 → 依赖虚拟内存机制旧页面在搬迁完成前保持可读或由读屏障引导不会读到半成品对象。关键在修复后写回原槽位——自愈保证每个槽位最多慢路径一次。Q4Marked0/Marked1 两个颜色位为什么需要两个ZGC 的周期是循环无缝衔接的。用两个位交替表示本轮/上轮存活切换周期时只是颜色语义翻转无需清理任何堆外状态或堆内标记——直接开始下一轮。若只有一个位则每轮开始前要清零全局标记就得引入 STW 或复杂并发协议。Q5ZGC 为什么吞吐低如何缓解三个来源① 每次引用加载多几条指令读屏障② 不分代时全堆并发遍历大量死对象反复被标③ 搬迁期间新旧内存并存堆余量需求大。缓解升级 JDK 21 用分代 ZGC官方基准已达到 G1 ±5% 吞吐、加大堆余量、调大 ZAllocationSpikeTolerance 应对分配尖峰。Q6分代 ZGC 怎么在没有写屏障/卡表的情况下做 Young GC 的根Young GC 的根 线程栈 全局引用照常扫 老年代指向新生代的引用关键。后者不靠记录而是依靠读屏障的兜底语义 新生代染色约定老→新引用指针在读时被屏障校验/修复配合年轻代对象地址可识别分代后地址空间按代划分Young GC 可直接精确定位不需全堆遍历也不需维护反向记忆集。这是用读屏障的天然一致性维护替代写屏障的元数据维护的教科书案例。Q7ZGC 和 Shenandoah 的本质区别ZGC元数据在指针上染色位对象搬走后靠转发表 指针自愈零对象级额外内存依赖平台指针布局。Shenandoah元数据在对象上转发指针每个对象多一个指针字访问都要走一次间接可移植性强。工程结论两者停顿性能同档选择更多取决于 JDK 发行版与团队运维生态。十一、总结ZGC 的技术演进可以概括为一条主线把 STW 的工作一项一项搬到并发世界。CMS 把标记并发化但清除是并发、整理不存在 → 碎片无解G1 把回收范围碎片化Region Mixed GC但 Evacuation 依然 STW → 停顿压不进几十毫秒ZGC 把最后的硬骨头对象搬迁并发化靠染色指针 读屏障 转发表 自愈让 STW 只剩根扫描 →停顿与堆彻底解耦分代 ZGCJDK 21补上分代假设的吞吐短板宣告低延迟与高吞吐不可兼得的时代结束选型建议落到一句话JDK 21、堆 32G、停顿 10ms 是 ZGC 的主场4~32G、停顿 100ms 级仍是 G1 的主场吞吐型离线任务用 Parallel。面试叙事线从为什么对象移动必须 STW这个根问题出发讲读屏障自愈如何打破它——这是把 ZGC 讲透的最短路径。
返回列表