ZGC:着色指针、读屏障与并发重定位
学完你要能自己回答
- 着色指针把哪些状态放进了引用?为什么要多重映射?
- 读屏障的快路径和慢路径分别做什么?
- 并发重定位如何找到对象的新地址?什么时候重映射?
- JDK 11 的 ZGC 有哪些硬限制,什么时候不该选它?
先看它要解决什么问题01
JDK 11 的默认 GC 是 G1。G1 已经把大部分标记和清理做成并发,但 Young GC 和 Mixed GC 仍需暂停;堆越大、存活对象越多,暂停越难稳定压在毫秒级。ZGC 的目标是让暂停时间不再随堆大小和存活对象数量线性增长。
| 维度 | G1(11 默认) | ZGC(11 实验) |
|---|---|---|
| 堆范围 | 小到中大 | 8MB–4TB |
| 暂停目标 | 可配置,默认 200ms | 不超过 10ms |
| 并发整理 | 部分阶段并发 | 标记与重定位都并发 |
| 分代 | 有 Young / Old | 单代 |
| 压缩 oops | 支持 | 不支持 |
| 类卸载 | 支持 | 11 不支持 |
| 平台 | 全平台 | 仅 Linux/x64 |
JDK 11 的 ZGC 是实验特性(JEP 333),不是默认 GC,也不能在 Windows/macOS 上启动。它解决的是大堆低延迟,不是所有场景的吞吐。
着色指针:把状态放进地址位02
传统 GC 用对象头或额外的标记位图记录「是否已标记」「是否已移动」。ZGC 换了个位置:把 64 位引用中的 4 位拿来做颜色标记,剩下的位表示对象地址。
| 颜色位 | 含义 |
|---|---|
| marked0 | 在第一次标记视图中已标记 |
| marked1 | 在第二次标记视图中已标记 |
| remapped | 引用已指向对象当前位置 |
| finalizable | 对象处于 finalization 相关状态 |
同一份堆内存在虚拟地址空间中被映射三次,分别对应 marked0、marked1、remapped 三个视图。给指针「染色」只需要换一个视图,不需要改物理地址;物理内存仍然只有一份。
| 项目 | JDK 11 的实现 |
|---|---|
| 地址位 | 42 位,最大 4TB 堆 |
| 颜色位 | 4 位 |
| 视图 | marked0 / marked1 / remapped 三重映射 |
| 指针宽度 | 始终 64 位,不支持压缩 oops |
一次 ZGC 周期只有三个暂停点03
ZGC 把一次回收拆成「暂停做根、并发做全堆」:标记和重定位的绝大部分工作与应用线程同时运行,STW 只保留三个很短的阶段。
- Pause Mark Start:暂停,扫描 GC roots,确定初始存活集合。
- Concurrent Mark / Remap:并发标记对象,同时重映射上一轮遗留的引用。
- Pause Mark End:暂停,处理弱引用、JNI 等边界,完成标记。
- Concurrent Prepare for Relocate:并发统计页存活率,选择重定位集。
- Pause Relocate Start:暂停,重定位根集合中的对象。
- Concurrent Relocate:并发复制剩余存活对象,修正引用。
Mark Start 和 Relocate Start 都需要扫描根集合,所以根越多、线程栈越深,这两个暂停越长。ZGC 把「堆越大暂停越长」变成了「根越多暂停越长」。
ZGC 11 是单代:没有 Young/Old 之分,所有对象在同一个周期里处理。周期模型简单,代价是短命对象也要被标记;分配速率高时更容易出现分配停顿。
读屏障:每次加载引用都过一道检查01
读屏障(load barrier)插在每次从堆中加载引用之后。应用线程拿到一个引用时,先检查它的颜色位:颜色与当前阶段匹配就走快路径;不匹配就进入慢路径,由慢路径完成标记、跟随转发指针或重映射。
| 路径 | 触发条件 | 动作 |
|---|---|---|
| 快路径 | 引用颜色与当前阶段匹配 | 直接返回,只有少量指令 |
| 慢路径 | 未标记 / 待重映射 / 待重定位 | 标记对象、跟随转发指针、修正引用 |
读屏障的关键作用是让「修正引用」可以延迟:应用线程什么时候读到旧引用,就什么时候把它修正到新地址。这样重定位不必暂停整个世界,只需在根修正时短暂 STW。
代价也很直接:每次引用加载都多了一次颜色检查。ZGC 用吞吐和 CPU 换低暂停,读屏障就是这个交换的落点。
并发重定位:转发指针与惰性重映射02
重定位把存活对象从 From 页复制到 To 页。复制完成后,ZGC 在 From 页的原对象里留下转发指针;之后任何线程通过旧引用访问,读屏障都会跟随转发指针到新地址,并把引用改写成新地址。
- 选择重定位集:按页统计存活率,优先回收垃圾比例高的页。
- 复制对象:并发进行,应用线程可以继续分配和访问。
- 安装转发指针:旧地址保留转发信息,直到所有引用被修正。
- 惰性重映射:读屏障在访问时修正引用;下一个标记周期会把剩余引用全部重映射。
转发指针放在 From 空间的对象里,而不是额外的全局表。查找新地址只需要一次内存访问,但 From 页必须等所有引用都重映射后才能释放。
ZPage:分配、回收与内存管理03
ZGC 的堆由 ZPage 组成,按对象大小分为小、中、大三档。页既是分配单位,也是重定位和回收的单位。
| 页类型 | 大小 | 用途 |
|---|---|---|
| Small | 2MB | 小对象 |
| Medium | 32MB | 中等对象 |
| Large | 2MB 的整数倍 | 大对象,独占页 |
回收的页进入页缓存,按大小分类保存,供后续分配复用;缓存过大时再归还给操作系统。ZGC 还会按 NUMA 节点组织页,尽量让对象分配在访问它的线程附近。
- 11 不支持类卸载:类元数据不会被 ZGC 回收,频繁生成类的应用要单独评估 Metaspace。
- 11 不支持压缩 oops:对象引用固定 64 位,堆内引用和缓存占用更高。
- 11 是单代:没有年轻代/老年代,也没有跨代引用处理。
在 JDK 11 打开 ZGC01
JDK 11 的 ZGC 是实验选项,必须先解锁实验选项,再选择 GC。堆大小和日志参数与 G1 类似。
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xmx16g -Xlog:gc* App
本机验证环境是 Windows,JDK 11 的 ZGC 只支持 Linux/x64,所以上面的命令无法在本机执行。不加解锁选项时,JVM 先拒绝实验选项:
java -XX:+UseZGC -version
Error: VM option 'UseZGC' is experimental and must be enabled via -XX:+UnlockExperimentalVMOptions. Error: The unlock option must precede 'UseZGC'. Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit.
加上解锁选项后,才会暴露真正的平台限制:
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -version
Error occurred during initialization of VM Option -XX:+UseZGC not supported
看什么日志,怎么判断02
ZGC 的日志同样走统一日志框架。重点看 Pause 行、堆前后变化、分配停顿和页缓存,而不是只盯总吞吐。
| 观察项 | 日志 / 命令 | 判断 |
|---|---|---|
| STW 暂停 | -Xlog:gc* 中的 Pause 行 | 是否稳定在目标范围内 |
| 堆占用 | -Xlog:gc 的 before->after | 是否存在持续上涨 |
| 分配停顿 | 日志中的 Allocation Stall | 堆太小或分配速率过高 |
| 页缓存 | -Xlog:gc+heap=debug | 缓存是否长期不归还 |
| 堆信息 | jcmd <pid> GC.heap_info | 确认页与堆的实际占用 |
Linux 上可以把日志按大小滚动,避免长时间运行写满磁盘:
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC \
-Xmx16g -Xlog:gc*:file=zgc.log:time,uptime:filecount=5,filesize=20M \
-jar app.jar
什么时候用,什么时候别用03
ZGC 适合堆大、停顿敏感、吞吐可以让步的服务;不适合追求极致吞吐、堆很小、或依赖类卸载的场景。
| 场景 | 建议 | 原因 |
|---|---|---|
| 常规 Web 服务 | 先用 G1 | 默认、全平台、分代、支持类卸载 |
| 32GB 以上大堆、停顿敏感 | 评估 ZGC | 暂停与堆大小解耦,代价是读屏障 |
| Linux/x64 以外的平台 | 不能用 ZGC | JDK 11 只支持 Linux/x64 |
| 频繁生成类 | 慎用 ZGC 11 | 11 不支持类卸载,Metaspace 可能持续上涨 |
| 性能测试、短生命周期 | 可考虑 Epsilon | 不回收,堆满即失败 |
- 选 G1:绝大多数 11 应用的默认答案,尤其是堆不大、依赖类卸载或压缩 oops 的场景。
- 选 ZGC:大堆、低延迟、Linux/x64,且能接受读屏障开销与单代限制。
- 选 Epsilon:只做性能测试或极短生命周期任务,明确知道不会 OOM;它不是「更快的 GC」。