分代 ZGC 深度解析从染色指针到双代并发回收适用版本JDK 21分代模式 JEP 439/ JDK 23默认模式 JEP 474关键词染色指针、读屏障、并发转移、记忆集、弱分代假设一、版本演进时间线版本变化JEPJDK 11ZGC 实验性引入单代架构JEP 333JDK 13最大堆从 4TB 扩展到 16TB—JDK 15ZGC 正式转正ProductionJEP 377JDK 21分代 ZGC 引入需-XX:ZGenerational显式开启JEP 439JDK 23分代成为默认非分代模式废弃JEP 474JDK 24非分代实现代码彻底移除—从 JDK 24 起“ZGC” 就只指分代 ZGC。二、为什么单代 ZGC 需要分代单代 ZGC 的目标是停顿时间不超过 10 ms实际通常 1 ms且停顿与堆大小无关。它通过染色指针 读屏障把标记、转移、指针修正全部并发化确实做到了亚毫秒停顿。但它有一个结构性软肋单代 ZGC 的两大痛点每轮 GC 都要处理全堆不管对象死活所有存活对象每轮都要重新标记一遍。堆越大、存活集越大每轮 GC 的 CPU 开销越高——这些 CPU 本可以跑业务线程。分配停顿Allocation Stall这是最疼的一个。业务线程分配速度一旦超过并发回收释放内存的速度分配请求就会被强制等待——这个等待不是 STW但对请求延迟的影响和 STW 一样致命而且可能持续几百毫秒。单代大堆 高分配速率的服务的典型症状就是 P99.9 毛刺。分代的解法弱分代假设绝大多数对象朝生夕死。把堆切成两个代年轻代小高频回收回收时只扫年轻代 记忆集单轮成本极低能快速释放内存跟上分配速率老年代大低频回收老年代里垃圾比例低、且增长慢全堆并发标记的次数大幅减少。结果就是单轮 GC 变小变快、GC 总 CPU 开销显著下降、分配停顿大幅减少——官方 JEP 439 给出的典型数据是堆占用约减半、吞吐约提升 10%。三、三大基石机制单代与分代共享3.1 染色指针Colored PointersZGC 的对象引用本身携带 GC 元数据。64 位指针的布局JDK 1364 位指针布局JDK 1363-48未使用47Final46Remap45Mark144Mark043-0对象地址16TBMarked0 / Marked1两轮标记交替使用区分本轮标记过与上轮标记过Remapped指针已完成转移后的修正状态Finalizable仅与 Finalizer 可达对象相关。配合多重映射multi-mapping同一份物理内存被映射到三个虚拟地址视图Marked0 视图、Marked1 视图、Remapped 视图。GC 阶段切换 切换当前好颜色指针数值不变、语义改变从而避免全堆遍历改写引用。代价RSS 虚高同一物理内存映射多次、仅支持 64 位、不支持压缩指针CompressedOops。3.2 读屏障Load BarrierJIT 在每次加载对象字段引用的代码处内联插入几条检查指令匹配不匹配加载引用颜色匹配当前视图快路径直接使用慢路径补标记 / 查转发表取新地址自愈self-heal修正后回写关键设计是自愈self-healing慢路径把指针修好后回写同一线程对同一引用的后续访问全部回到快路径修复成本被摊薄。正是读屏障让标记、转移、指针修正能与应用线程真正并发执行——这是 ZGC 停顿与堆大小无关的根源。3.3 并发转移Concurrent Relocation每轮 GC 选取垃圾密度高的页组成转移集Relocation Set存活对象被并发复制到新页旧页整页丢弃——天然完成碎片整理转发表Forwarding Table记录对象旧地址 → 新地址读屏障据此完成引用修正。四、分代架构设计4.1 堆结构存活对象整体晋升无 survivor、无年龄阈值老年代占大头regionregionregion...年轻代占比小自适应regionregion...大对象页全堆仍由**等大 Region页**组成页的类型小/中/大与代独立小对象经 TLAB 在年轻代无锁快速分配大对象直接进对应页Young GC 判定死亡的页整页回收复用不做逐对象清理。4.2 晋升策略极简主义这是与 G1/CMS 最大的概念差异之一G1 / CMS分代 ZGC新生代细分Eden 两个 Survivor无细分年龄计数Age table逐代复制无年龄计数晋升条件达到-XX:MaxTenuringThreshold存活过一轮 Young GC 即整体晋升动态年龄推断有无ZGC 的哲学与其在年轻代里反复倒腾短命对象不如让年轻代保持极小一次复制直接到位。4.3 跨代引用与记忆集Remembered Set——分代的核心代价分代后Young GC 面临“老年代里哪些对象指着年轻代”的问题。朴素解法是扫全堆——那就白分代了。ZGC 的方案写屏障 记忆集JIT 在字段写入处插入 store barrier。当检测到“老年代对象的字段被写入了一个年轻代引用”时将该对象所在的页登记进记忆集按页粒度记忆集本身分配在堆里允许假阳性记忆集里可能登记了已经不再指向年轻代的页——多扫一点无妨只影响效率不影响正确性Young GC 的根集合 GC roots 年轻代对象图 记忆集指向的老年代页绝不扫整个老年代。代价写屏障带来少量运行时开销记忆集带来额外内存占用——这是分代 ZGC 相对单代的主要 trade-off。4.4 Young GC 并发周期与应用线程并发初始标记STW 1ms并发标记结束标记STW 1ms转移准备并发转移并发再映射*仅有的两个 STW 只处理根集合扫描时长只与线程数/根数量相关与堆大小、存活集大小无关标记根集合 GC roots 年轻代 记忆集Old GC 是独立的全堆并发周期频率低可与 Young GC 穿插进行。五、分代 ZGC vs 单代 ZGC 全面对比维度单代 ZGC分代 ZGC引入版本JDK 11JEP 333JDK 21JEP 439当前状态JDK 23 废弃JDK 24 移除JDK 23 唯一形态开启方式-XX:UseZGCJDK ≤ 22JDK 21/22 需加-XX:ZGenerationalJDK 23 直接-XX:UseZGC堆划分单一代全堆 Region年轻代 老年代比例自适应单轮 GC 范围全堆Young GC 只扫年轻代 记忆集GC 频率每轮都是全量Young GC 高频小额 Old GC 低频全量每轮 GC CPU 开销高全堆标记转移低大多数轮次只碰年轻代存活对象处理每轮全部重新标记年轻代存活一轮即晋升老年代极少重复标记跨代引用不存在该问题写屏障 页粒度记忆集写屏障开销无有少量运行时成本记忆集内存开销无有晋升机制不适用存活过一轮即整体晋升无 survivor、无年龄阈值停顿时间亚毫秒亚毫秒同样与堆大小无关分配停顿风险高分配速率下明显大幅减少年轻代高频回收跟上分配速率堆内存占用高垃圾需等全堆 GC 才释放约减半官方典型值吞吐量基准约 10%官方典型值调优复杂度参数少但可用手段有限大堆下基本靠加堆基本免调代际比例自适应JDK 21 限制功能完整不支持ZAllocationSpikeTolerance、ZProactive、ZUncommit等后续版本补齐适用场景已无理由选择大堆 低延迟 高分配速率收益可视化归一化官方 JEP 439 典型值指标单代 ZGC分代 ZGC堆内存占用████████████████████ 100██████████ ~50吞吐量████████████████████ 100██████████████████████ ~110一句话总结差异单代 ZGC 解决了“停顿与堆大小无关”分代 ZGC 在此基础上解决了“GC 的 CPU 开销与堆大小成比例”的问题——用弱分代假设把绝大多数回收工作压缩到一个小得多的年轻代里完成。六、参数与实战# JDK 21 / 22显式开启分代模式java-XX:UseZGC-XX:ZGenerational-jarapp.jar# JDK 23默认即分代java-XX:UseZGC-Xms32g-Xmx32g-XX:UseLargePages-jarapp.jar# 常用配套-XX:SoftMaxHeapSize24g# 软上限GC 尽力把用量压回该值以下-XX:ConcGCThreads4# 并发 GC 线程数默认约核数的 1/4-XX:ParallelGCThreads8# STW 阶段并行线程数实战注意事项不要手动设代际比例——ZGC 自适应这是与 G1 使用习惯最大的差异JDK 21 首发的分代模式部分调优参数不可用升级前用-XX:PrintFlagsFinal核对生产环境强烈建议开大页-XX:UseLargePages多重映射对 TLB/页表压力敏感监控不要裸看进程 RSS多重映射虚高用 JFR 的 ZGC 事件或jstat -gcutil选型堆 8GB 或追求极致吞吐 → G1数十 GB 大堆 P99 延迟敏感交易、风控、实时行情→ 分代 ZGC。七、面试要点速记一句话定位分代 ZGC 染色指针 读屏障 并发转移单代三基石之上叠加弱分代假设 写屏障记忆集为什么停顿与堆大小无关STW 阶段只扫根集合标记、转移、指针修正全部并发外包给读屏障为什么单代不够全堆标记 CPU 开销随堆线性增长 分配停顿风险分代的新问题跨代引用 → 页粒度记忆集允许假阳性、只影响效率不影响正确性有数字堆占用约减半、吞吐约 10%、停顿持续亚毫秒有版本21 引入、23 默认、24 移除非分代。