# 《新一代垃圾回收器ZGC设计与实现》知识点全解 > AI生成|2026年10月29日|codebuddy|原书:彭成寒《新一代垃圾回收器ZGC设计与实现》,机械工业出版社,2019-07-01 > **我的阅读**:进度 100%|累计 3小时32分钟|开始 2020-07-02|最近 2021-04-21|读完 2020-07-06 ## 一句话总结 这本书回答一个问题:**当堆大到 TB 级、又要求暂停时间稳定在 10ms 以内时,传统分代+写屏障的回收器为什么撑不住,ZGC 用「着色指针 + 多视图映射 + 读屏障 + 并发转移」这套组合如何把停顿从「与堆大小成正比」压成「与堆大小基本无关」。** ## 适合谁 / 前置知识 **适合**:已有 Java 应用调优经验、想弄懂 ZGC 内部机制的后端/基础架构工程师;需要为大内存服务选型 GC 的架构师;准备读 OpenJDK 源码但被 `zColoredPointer`、`ZForwarding` 这类符号劝退的人。 **不太适合**:刚学 Java、还没搞清堆/栈/引用类型的人——书里默认你懂对象头、可达性分析、三色标记、卡表与写屏障。若这些不熟,先补一本 JVM 入门书再回来。 **前置清单**:① 对象在堆上的布局与 Mark Word;② 可达性分析与三色标记的不变式;③ CMS/G1 的并发标记与 SATB、增量更新两种并发方案;④ 虚拟内存、页表、mmap 的基本概念(第 2 章几乎全建立在这上面)。 ## 一、全书地图 全书是「**背景 → 地基 → 执行者 → 算法设计 → 算法实现 → 观测 → 调优 → 展望 → 工具**」的线性推进,第 2、4、5 章是硬核主体。 | 章 | 主题 | 在全书中承担的角色 | 与其他章的关系 | |---|---|---|---| | 第1章 | 垃圾回收器概述 | 建立坐标系:回收算法与历代回收器 | 1.2.5 是全书摘要,先读它可预览全局 | | 第2章 | ZGC 内存管理 | **地基**:地址空间、多视图映射、页面、NUMA | 不懂多视图映射,第4章的着色指针就是天书 | | 第3章 | ZGC 线程 | **执行者**:控制线程、工作线程、触发时机 | 解释第5章各阶段"由谁在什么条件下发起" | | 第4章 | 回收算法的设计 | **原理**:并发算法与并发处理 | 第5章是它的工程落地,两章应连读 | | 第5章 | 回收算法的实现 | **主体**:一次完整 GC 的 11 个步骤 | 每一步都对应第6章的一段日志 | | 第6章 | ZGC 日志解读 | **观测**:Xlog 与各阶段/统计信息 | 用第5章的阶段名做索引去读日志 | | 第7章 | 参数和基准测试 | **调优**:参数语义与压测方法 | 参数作用于第3章触发时机、第5章各阶段 | | 第8章 | 发展与展望 | 边界:类回收、单代回收、新平台 | 说明 2019 年版本的已知短板 | | 第9章 | JVM 编译调试 | 工具:如何编译并调试 OpenJDK | 服务于想读源码的人,可独立阅读 | **两条推荐读法**:想快速建立直觉 → 1.2.5 → 第4章 → 第6章;想彻底搞懂 → 第2章 → 第3章 → 第4章 → 第5章 → 第6章,第7章当手册查。 ## 二、核心知识点 ### 1. 着色指针(Colored Pointer) **是什么**:把 GC 状态直接编码进 64 位对象引用本身,而不是放在对象头里。ZGC 在指针中划出若干高位作为标志位:`Marked0`、`Marked1`、`Remapped`、`Finalizable`,其余位表示对象地址。 **为什么重要**:状态跟着引用走,意味着「读到一个引用」的瞬间就能判断它是否需要修正,不需要额外的表结构,也不需要访问对象头——这是读屏障能做得极轻的前提,也是 ZGC 放弃分代、放弃卡表的底气。 **怎么用/例子**:两个 Marked 位交替使用。第 N 轮 GC 用 Marked0 表示"本轮已标记",第 N+1 轮换成 Marked1。这样新一轮开始时**不必遍历全堆清标记位**,只要切换"哪个位算有效"即可,省掉一次与堆大小成正比的 STW。 **常见误区**:以为染色只是"打标记的另一种写法"。真正的代价是**指针不再是纯地址**:任何直接把引用当地址用的地方(JNI、压缩指针、某些 Unsafe 用法)都要特殊处理;同时它限制了可寻址范围(JDK11 时期堆上限 4TB 量级)。 ### 2. 多视图映射(Multi-Mapping) **是什么**:把**同一块物理内存**在虚拟地址空间里映射成三份,分别对应 Marked0、Marked1、Remapped 三种"颜色视图"。指针的高位决定它落在哪个视图,三个视图指向同一份数据。 **为什么重要**:如果没有多视图,改变一个引用的颜色就必须真的去修改这个引用所在的字段——那就退化成写屏障方案。有了多视图,"给指针换个颜色"只需要**改指针本身的值**(换一段虚拟地址),物理内存一个字节都不用动;而三个视图读到的又是同一份对象,语义完全一致。 **怎么用/例子**:`obj.field` 里存的是 Marked0 视图下的地址;GC 把它 remap 后写回成 Remapped 视图下的地址,字段所在对象不变,读到的仍是同一个对象。 **常见误区**:把"三个视图"理解成"三份拷贝"。恰恰相反,是**一份物理内存、三份虚拟映射**,所以不占三倍内存,只占三倍虚拟地址空间——这正是 64 位地址空间才玩得起的技术。 ### 3. 页面(ZPage)与三种规格 **是什么**:ZGC 把堆切成页,按对象大小分三档:**小页面(Small,2MB)**、**中页面(Medium,32MB)**、**大页面(Large,为 2MB 的整数倍,恰好容纳一个大对象)**。对象按大小进入对应规格的页面,页面内按顺序分配。 **为什么重要**:① 大对象单独占页,避免"一个大数组把整个区域撑爆"的碎片问题;② 回收以**页**为单位,不移动页内活对象就无法释放页面,于是"页内垃圾比例"成了选择回收页面的核心指标;③ 页的粒度决定了转移的成本上限。 **怎么用/例子**:一个 5MB 的数组会落在 Medium 页;一个 40MB 的数组独占一个 Large 页。前者可与同页其他对象一起被转移,后者只能整页处理。 **常见误区**:拿 G1 的 Region 类比。G1 Region 大小固定且数量有限,还有跨 Region 引用要维护 RSet;ZGC 的页没有 RSet,靠读屏障现场判断,代价模型完全不同。 ### 4. 读屏障(Load Barrier) **是什么**:在**读取引用**的路径上插入一小段代码:检查读到的指针颜色是否"好"(当前轮次有效)。好则直接返回;不好则进慢路径——修正指针、必要时标记对象,然后返回正确引用。写操作不加屏障。 **为什么重要**:这是 ZGC 一切并发的支点。并发转移时,应用线程可能读到"指向旧地址"的引用,读屏障当场把它改成新地址(**自愈,self-healing**),于是"转移"这件事对应用线程是透明的,不需要 STW 去修补所有引用。 **怎么用/例子**:应用线程执行 `x = obj.next`,若 `obj.next` 还是 Remapped 颜色而当前处于标记阶段,读屏障会发现颜色不匹配,把 `obj.next` 就地改成 Marked 颜色并返回。下一次读同一字段就命中快路径。 **常见误区**:① 以为读屏障很贵——快路径通常只是几条比较与跳转,成本摊在每次引用读上,属于"低幅度、全命中"的税;② 以为 ZGC 也用写屏障/SATB 记录旧值——那套是 G1、CMS 的路子,ZGC 不用。 ### 5. 并发标记与三色不变式 **是什么**:标记从根集合出发遍历对象图,期间应用线程不停。ZGC 靠读屏障在"读"的瞬间补标记,维持"黑色对象不能指向白色对象"的不变式。 **为什么重要**:并发标记的敌人是"标记完成后又冒出来的活对象"。传统方案用写屏障记录变更(SATB 记旧值 / 增量更新记新值),ZGC 把这件事挪到读屏障上:只要有人读,就读到正确颜色,顺带把该标的补上,漏标窗口被压到极小。 **怎么用/例子**:标记阶段应用线程把一个白对象挂到黑对象下,若之后有人读到这条引用,读屏障会发现它没被标记并当场标记;若永远没人读它,它本来也不影响后续回收的正确性。 **常见误区**:把 ZGC 简单归为"SATB 实现"。它既不是教科书式 SATB 也不是纯增量更新,而是**读屏障驱动的标记**,这是理解后续"再标记为什么很短"的关键。 ### 6. 一次 GC 的完整阶段 **是什么**:JDK11 的 ZGC 一轮回收大致是:**初始标记(STW)→ 并发标记/重映射 → 再标记(STW)→ 并发准备转移 → 初始转移(STW)→ 并发转移**。三个 STW 点都极短。 **为什么重要**:停顿只出现在"根集合扫描""标记收尾""转移集初始化"这类**与堆大小无关或弱相关**的环节;真正耗时的对象图遍历与对象搬运全部并发化。这就是"停顿不随堆增长"的结构性原因。 **怎么用/例子**:读日志时按阶段名对齐:`Pause Mark Start` 对应初始标记,`Pause Mark End` 对应再标记,`Pause Relocate Start` 对应初始转移。三个 Pause 的时长就是用户感知的停顿。 **常见误区**:以为"并发"等于"零停顿"。ZGC 仍有 3 个 STW 点,只是每个都被压到毫秒级;且并发阶段会与应用线程抢 CPU,吞吐量必然有损。 ### 7. 初始标记:为什么它必须 STW 却依然快 **是什么**:暂停应用线程,扫描根集合(线程栈、JNI 全局引用、静态字段等),把根直接引用的对象标记出来,并建立后续并发标记的起点。 **为什么重要**:根集合是"必须有一致快照"的地方——扫描时栈在变,只能停下来。但根集合的规模取决于**线程数与栈深**,不取决于堆大小,所以这一步的耗时与 TB 级堆无关。 **怎么用/例子**:线程数多、栈很深的服务,初始标记会相对更慢;减少线程数或控制栈深度可间接缩短它。 **常见误区**:把初始标记当成"标记了整个对象图"。它只处理根的直接邻居,剩下的是并发标记的活。 ### 8. 再标记与非强根并行标记 **是什么**:并发标记收尾时再停一次,处理漏标与 SATB 类残留;随后**非强引用**(软、弱、虚、finalizer 可达)单独用并发阶段处理。 **为什么重要**:非强引用的处理语义复杂(软引用要看内存压力、虚引用要挂队列、finalizer 要排队执行),放进 STW 会显著拉长停顿,因此 ZGC 把它拆成独立的并发阶段。 **怎么用/例子**:大量使用 `WeakHashMap` 缓存的应用,会在此阶段看到明显耗时;日志里对应"非强根并行标记"与"引用并发处理"段落。 **常见误区**:以为弱引用会被"立刻"清掉。清理发生在该并发阶段,应用可能在短暂窗口内仍读到一个"逻辑上该消失"的引用。 ### 9. 转发表与并发转移 **是什么**:转移(Relocate)把活对象从"垃圾多的页"搬到"空闲页"。每个待转移页维护一张**转发表**,记录"旧地址 → 新地址";尚未搬完时,读屏障查表把旧地址换成新地址。 **为什么重要**:转发表让"搬运"和"访问"可以同时发生——应用线程读到旧地址不要紧,查表即得新地址并自愈。这是 ZGC 能做到"边搬边用"的机制核心。 **怎么用/例子**:`Pause Relocate Start` 只做"选定转移集、初始化转发表",真正的拷贝在并发转移阶段完成。 **常见误区**:以为转发表是全局大表。它是**按页组织**的,页面回收后即可丢弃,不会长期膨胀。 ### 10. 页面选择与回收 **是什么**:一轮结束后统计每页的存活字节,把"垃圾比例高"的页选入转移集,搬走其中少量活对象后整页释放。 **为什么重要**:ZGC 不做压缩式的整体整理,而是**按页择优回收**;选页策略直接决定"每轮能还回多少内存"和"要搬多少对象"——搬得越多,并发转移越久,CPU 争用越明显。 **怎么用/例子**:`ZCollectionInterval`、`ZAllocationSpikeTolerance` 影响触发频率;触发太频繁会导致"每轮可回收的垃圾不够多",白交并发开销。 **常见误区**:指望 ZGC 像 G1 那样"自动保持低碎片"。ZGC 靠页级回收而非对象级压缩,长期运行后中小页的碎片仍需观察。 ### 11. 触发时机与三类入口 **是什么**:GC 由三条路径触发:**分配速率触发的自适应阈值**、**定时触发**(`ZCollectionInterval`)、**外部显式请求**(如 `System.gc()`)。第3章用"时钟触发器 / 消息触发 / VMThread"描述这套调度。 **为什么重要**:ZGC 的默认策略是"在内存还够时就提前回收",用**并发**换**停顿**。若等到堆快满才触发,并发转移来不及,就会退化成频繁 STW 或 OOM。 **怎么用/例子**:稳态服务用默认自适应即可;批处理可以在空闲窗口用 `ZCollectionInterval` 定时回收,避开业务高峰。 **常见误区**:把 `ZCollectionInterval` 当成"每隔 N 秒必须 GC"。它是"至少间隔 N 秒允许触发",实际仍受分配情况约束。 ### 12. 关键参数与容量规划 **是什么**:核心几个:`-XX:+UseZGC` 启用;`-Xmx`/`-XX:ZHeap` 定堆;`ZUncommit` 控制是否把空闲内存还给 OS;`ZUncommitDelay` 控制归还延迟;`ZCollectionInterval` 定时触发;`ZAllocationSpikeTolerance` 调节对分配尖峰的容忍度;`ZProactive` 控制主动回收。 **为什么重要**:ZGC 的低停顿是**用内存和 CPU 换来的**:并发标记/转移需要额外空间容纳"转移中的对象",堆太紧会直接触发退化。经验上要给 ZGC 留出比 G1 更宽裕的堆。 **怎么用/例子**:先固定 `-Xmx` 做基线压测,再逐步下调观察 P99 停顿与 GC 频率;不要一上来就调 `ZAllocationSpikeTolerance`,先把堆大小和触发频率调顺。 **常见误区**:只盯"最大停顿"这一个数。真正该看的是**停顿分布(P99/P999)+ 吞吐下降 + 实际占用内存**三者的权衡。 ### 13. 用 Xlog 读 ZGC **是什么**:`-Xlog:gc*` 输出初始化信息、触发原因、各阶段耗时、以及统计段(回收器信息、竞争、同步等待、内存、步骤、子阶段、线程)。 **为什么重要**:ZGC 的并发特性让"总耗时"毫无意义,必须看**每个阶段的并发/停顿归属**。日志里的 `Concurrent` 与 `Pause` 前缀就是区分依据。 **怎么用/例子**:排查"停顿变长"→ 只看 Pause 段;排查"吞吐下降"→ 看并发阶段的 CPU 竞争与同步等待;排查"内存不还"→ 看内存段与 uncommit 记录。 **常见误区**:拿 G1 的日志阅读习惯套 ZGC,把并发阶段的时间也算进停顿。 ### 14. ZGC 与 Shenandoah 的取舍 **是什么**:两者目标一致(低停顿、大堆),路径不同:ZGC 用**着色指针 + 多视图映射 + 读屏障**;Shenandoah 用** Brooks 转发指针 + 读/写屏障**,不依赖染色指针,因而对地址空间和平台的要求更宽松。 **为什么重要**:选型不是"谁更先进",而是**平台与 JDK 版本约束**:ZGC 早期只在 Linux/x86-64 可用,Shenandoah 的平台适配更早更广;ZGC 的转发开销更低,Shenandoah 的实现更"通用"。 **怎么用/例子**:跨平台或非 x86 环境优先验证 Shenandoah 的可用性;纯 Linux x86-64 大堆服务优先试 ZGC。 **常见误区**:以为二者可以混用或互为替代参数。它们是不同回收器,参数体系不通用。 ### 15. 已知边界:类回收、单代与平台 **是什么**:书中第8章讨论三个方向:**类卸载(类回收)**在 ZGC 早期支持不完整、**单代回收**(无分代)带来的"每次都要扫全堆"成本、以及**新功能与多平台**支持。 **为什么重要**:这解释了为什么 ZGC 在"短生命周期对象极多"的应用上吞吐不如 G1——没有年轻代,弱分代假说带来的收益就吃不到;也解释了早期版本"仅 Linux/x86-64"的现实限制。 **怎么用/例子**:以"大量朝生夕死对象"为主的服务,先用 G1 做对照实验,再决定是否上 ZGC;需要类动态生成(脚本引擎、热部署)的应用要专门验证类卸载行为。 **常见误区**:把 ZGC 当"万能更快的 GC"。它是**停顿优先**的取舍,不是全面胜出。 ## 三、关键概念速查表 | 概念 | 一句话解释 | 用在哪儿 | |---|---|---| | 着色指针 | 把 GC 状态位编进 64 位引用 | 免查对象头即可判断引用是否需修正 | | Marked0 / Marked1 | 两个交替使用的标记位 | 换轮次时免去全堆清标记 | | Remapped | 表示引用已指向对象当前位置 | 读屏障快路径的"好颜色" | | Finalizable | 仅通过 finalizer 可达 | 标识待终结对象 | | 多视图映射 | 一份物理内存、三份虚拟映射 | 改颜色只需改指针,不动数据 | | ZPage | 堆的分配与回收单位 | 2MB 小页 / 32MB 中页 / N×2MB 大页 | | 读屏障 | 读引用时检查并修正颜色 | 并发标记与并发转移的支点 | | 自愈 | 读屏障顺手把旧引用改成新引用 | 免除转移后的全局引用修补 | | 转发表 | 页级的旧地址→新地址映射 | 转移未完成时定位对象 | | 转移集 | 本轮被选中要搬空的对象页集合 | 决定并发转移的工作量 | | 初始标记 | STW 扫根集合 | 建立并发标记起点 | | 再标记 | STW 收尾,处理漏标 | 保证标记完整性 | | 初始转移 | STW 初始化转移集与转发表 | 启动并发转移 | | 非强引用处理 | 软/弱/虚/finalizer 的并发处理 | 避免拉长 STW | | 类回收 | 卸载无用的类元数据 | 动态类场景的关键能力 | | Xlog | 统一的日志框架 | 分阶段定位停顿与吞吐问题 | | Shenandoah | 另一款低停顿回收器 | 跨平台替代方案 | ## 四、可复用的方法/框架 **框架 A:判断"一个 GC 为什么能低停顿"的四问** 1. **并发的是什么**——标记?转移?还是只有清理?(ZGC:前三者中的标记与转移都并发) 2. **靠什么维持一致性**——写屏障、读屏障,还是 STW 补刀?(决定额外开销摊在哪里) 3. **元数据存在哪**——对象头、旁路表、还是指针本身?(决定内存与缓存开销) 4. **哪些环节无法并发**——根扫描、收尾、转移集初始化?(这些就是停顿下限) 这四问可以套用到任何回收器,比背参数有用。 **框架 B:选型判断清单** - 堆是否真的大到"STW 与堆成正比"成为痛点?否则 G1 更划算。 - 应用是"长寿命大对象为主"还是"朝生夕死小对象为主"?后者要警惕无分代的吞吐损失。 - 平台与 JDK 版本是否支持?早期 ZGC 仅 Linux/x86-64。 - 能否接受 10%–15% 量级的吞吐下降换稳定低停顿? - 堆能否给得比 G1 更宽裕?并发转移需要腾挪空间。 - 是否有 JNI / Unsafe / 压缩指针等对着色指针敏感的用法? **框架 C:ZGC 调优顺序** 先定 `-Xmx`(给足)→ 观察 Pause 分布 → 看并发阶段是否被 CPU 饿死 → 再调触发策略(`ZCollectionInterval` / 主动回收)→ 最后才碰 uncommit 与尖峰容忍度。**顺序反了会得出一堆互相打架的结论。** ## 五、我的阅读轨迹 | 项目 | 数据 | |---|---| | 进度 | 100% | | 累计阅读 | 3小时32分钟 | | 开始阅读 | 2020-07-02 | | 最近阅读 | 2021-04-21 | | 读完时间 | 2020-07-06 | **解读与复习建议**:4 天读完、2021 年又回访过一次,说明当时是"通读建立框架",回访多半是为了某个具体问题(调参或日志)。3.5 小时读完一本 9 章的技术书,属于**框架级理解而非源码级掌握**——第 2 章多视图映射、第 5 章 11 个阶段的实现细节,很可能当时是"看懂了但没吃透"。 **建议复习路径**: 1. **第一优先**:第4章 + 第5章连读,用"一次 GC 的时间轴"把 11 个步骤串成一条线,能在白纸上默画出来为止。 2. **第二优先**:第2章的多视图映射,动手画一遍"三个虚拟地址 → 同一物理页"的图,这是全书最容易被跳过、也最影响后续理解的一节。 3. **第三优先**:第6章日志,找一段真实 `-Xlog:gc*` 输出,把每个 Pause 段与第5章的阶段名对上。 4. 第7、8、9 章当作查询与延伸,不必重读。 ## 六、行动清单 1. 用 JDK 17+ 起一个 `-XX:+UseZGC -Xmx8g` 的小服务,跑一轮压测并保存 `-Xlog:gc*` 原始日志。 2. 把日志里的三个 Pause 段时长单独提取成表,算出 P99 停顿,与 G1 同条件对比。 3. 在白纸上默画一次 ZGC 的完整阶段时间轴,标出哪几段是 STW、哪几段并发。 4. 手绘"着色指针 + 三视图映射"示意图,写清"改颜色为什么不动物理内存"。 5. 造一个 40MB 大对象与大量小对象的混合负载,观察 Large/Medium/Small 页的分配与回收差异。 6. 用 `WeakHashMap` 与 `finalize` 各写一个用例,观察非强引用处理阶段的耗时变化。 7. 整理一份团队版《ZGC 选型六问》清单,把第四节的框架 B 落成可勾选的表格。 8. 读一遍 Shenandoah 的转发指针方案,写 300 字对比笔记,明确两者平台约束差异。 ## 七、延伸阅读 1. **《深入理解Java虚拟机》(周志明)**——先读。它是坐标系:分代理论、三色标记、CMS/G1 的并发方案,都是本书的默认前提。**顺序:先这本,再本书。** 2. **《Java性能权威指南》(Scott Oaks)**——并读或后读。本书讲"ZGC 怎么实现",这本讲"GC 怎么调、指标怎么看",把机制转成调优动作。**顺序:本书之后。** 3. **《垃圾回收算法手册:自动内存管理的艺术》(Jones 等)**——后读、选读。算法层面的理论总纲,用于理解 ZGC 的取舍在学术谱系里的位置(读屏障、转发指针、并发复制)。**顺序:本书之后,按章查阅。** > 说明:本书目录中第5章"垃圾回收算法的实现"与第9章"JVM编译调试"均按原目录列出,未额外补充章节号;涉及具体 JDK 版本行为差异之处(如 ZGC 的分代支持、平台支持范围)以实际所用 JDK 版本文档为准,本文只做机制层面的描述。