染色指针是将gc状态信息编码至64位指针高4位,而非对象头;利用虚拟地址高位空闲位存储marked0、marked1、remapped、finalizable等标志,通过位运算快速提取地址与状态,避免对象头读写及内存屏障开销。

染色指针不是“给对象上色”,而是复用指针高位存储状态
很多人误以为 Colored Pointers 是在对象头里加标记位,其实完全相反:ZGC 把标记信息直接塞进 64 位指针本身。现代 Linux x86_64 虚拟地址空间只用低 42~48 位寻址(对应 4TB~256TB),高位长期空闲——ZGC 恰好借用了其中 4 个 bit(如 Marked0、Marked1、Remapped、Finalizable)来编码对象的 GC 阶段状态。
这样做绕开了传统 GC 必须读写对象头的开销,也避免了额外维护 bitmap 或 card table 的内存与同步成本。关键点在于:decode_pointer() 这类操作是纯位运算,毫秒级停顿里容不得函数调用或内存访问延迟。
- 实际地址提取必须用
ptr & ~0xF(屏蔽低 4 位),不能用ptr >> 4 ,后者可能引入分支或非对齐访问 - 不同 JVM 版本对“颜色位”定义略有差异,Java 15+ 的
Remapped位含义与 Java 11 不完全一致,调试时需查对应版本的zAddress.hpp - 染色指针仅在 ZGC 启用时生效;若误配
-XX:+UseG1GC却在代码里假设指针含颜色位,会直接导致地址错乱
读屏障 Load Barrier 不是“拦截器”,而是指针解码的强制检查点
Load Barrier 常被误解为类似 Spring AOP 的代理层,实际上它是 JVM 在每次执行 aload、getfield 等字节码时,由 JIT 编译器内联插入的一小段汇编逻辑。它的唯一职责是:当发现加载的指针带 Marked 位时,立即触发并发标记;若带 Remapped 位,则透明重定向到新地址并清除旧位。
这个过程不阻塞应用线程,但要求所有对象访问路径都经过它——这也是 ZGC 强制禁用 -XX:+UseCompressedOops 的原因:压缩指针会破坏高位可染色的前提。
- 读屏障开销集中在首次访问染色指针时,后续访问若已
Remapped则几乎无额外成本 - JIT 编译器会对连续字段访问做 barrier 合并优化,但手动写的
unsafe.getLong(obj, offset)会绕过 barrier,导致并发标记漏对象 - JNI 代码中若直接用
jobject地址做指针运算,同样跳过 barrier,必须改用GetObjectRefType等安全 API
16TB 堆不是靠“堆大”,而是靠分层页表 + 染色位解耦地址空间
ZGC 支持 16TB 堆的关键不在物理内存,而在虚拟地址管理方式。它把 64 位地址空间划分为三层页表结构,配合染色指针的 4-bit 状态域,让每个内存页能独立处于 Marked、Relocated、Remapped 等状态。这意味着 GC 线程可以并发处理不同页,而应用线程访问某一页时,仅需检查该页对应指针的颜色位,无需全局锁或全堆扫描。
这种设计使 STW 阶段只剩两个极短操作:初始根扫描(Root Scanning)和最终重映射修复(Fixup),两者耗时均与活跃对象数相关,而非堆总大小。
- 启用 16TB 堆必须显式指定
-Xmx16T,仅靠-XX:+UseZGC不会自动扩展上限 - Linux kernel 需开启
transparent_hugepage=never,否则 ZGC 的页表管理与 THP 冲突,引发随机长停顿 - 堆越大,ZGC 的并发线程数(
-XX:ConcGCThreads)越需调高,但超过 CPU 核心数 1.5 倍后收益递减,反而增加调度抖动
真正容易被忽略的停顿来源:元数据区与 JNI 全局引用
即使 ZGC 的主堆停顿压到 1ms 以内,生产环境仍可能观测到 20~50ms 的“伪停顿”。这些往往来自未被 ZGC 管理的区域:
-
Metaspace回收仍需 STW,尤其在动态生成大量类(如 Spring Boot + CGLIB)时,建议用-XX:MaxMetaspaceSize=512m限流 - JNI 全局引用(
GlobalRef)表扫描无法并发,频繁NewGlobalRef/DeleteGlobalRef会拖慢 finalization 阶段 - 第三方 agent(如某些 APM 工具)若 hook 了
ClassLoader.defineClass,可能隐式触发 class redefinition STW
分析这类问题不能只看 ZGC 日志,必须配合 -Xlog:gc+metaspace=debug 和 jstack -l 抓取停顿时的线程栈——真正的瓶颈常常藏在 GC 日志之外。











