ZGC(Z Garbage Collector)是Oracle在JDK 11中引入的一款实验性的低延迟垃圾收集器,并在JDK 15中正式发布。它的设计目标是:在任意堆内存大小下(从几百MB到数TB),将垃圾收集的停顿时间控制在10毫秒以内。这是一个极其雄心勃勃的目标,旨在解决G1和CMS在大堆应用上仍然可能产生较长停顿的问题。
核心目标与设计理念
ZGC的核心设计理念是几乎所有的垃圾收集工作都是并发执行的,从而将STW(Stop-The-World)停顿缩短到仅有的几个毫秒级阶段。为了实现这一目标,ZGC采用了两大基石技术:着色指针和读屏障。
核心技术:着色指针与读屏障
1. 着色指针 (Colored Pointers)
这是ZGC的基石。它颠覆了传统观念,在指针本身而不是在对象头上存储元数据信息。
在64位平台上,一个指针的理论寻址空间是2^64字节,这是一个极其巨大的空间。而实际上,现代硬件和操作系统并不会使用完所有64位。Linux/x86_64平台通常使用48位虚拟地址空间(高16位为符号扩展),而AMD64架构支持52位物理地址空间。ZGC巧妙地利用了这些未使用的指针位来存储信息。
ZGC使用了64位指针中的4个位(称为“元位”)来标记对象的状态:
- Finalizable:对象只能通过finalizer访问。
- Remapped:对象已被重映射(即新的地址已被记录)。
- Marked1:标记状态1。
- Marked0:标记状态0。
这些“颜色”标记了对象在GC周期中的状态。通过操作指针的颜色位,ZGC可以高效地并发执行标记和重定位(压缩)操作,而无需暂停所有应用线程。
2. 读屏障 (Load Barrier)
读屏障是JVM在从堆内存加载引用时插入的一小段代码。可以把它想象成一个“陷阱”或一个“钩子”。每当应用程序线程从堆中读取一个对象引用时(例如 Object foo = obj.field;),ZGC的读屏障就会被触发。
读屏障的核心作用:根据着色指针的颜色位,“治愈” 这个引用。
- 如果读到的指针颜色显示该对象尚未被处理(例如,尚未被标记或尚未被重定位),读屏障会主动介入,完成相应的处理工作(例如,标记对象或更新引用到新地址)。
- 如果指针颜色显示一切正常,则读屏障几乎不做任何事,开销极低。
正是“着色指针+读屏障”的结合,使得ZGC的标记和重定位两大最耗时的任务能够与应用线程并发执行。
ZGC的执行过程
ZGC的GC周期可以分为几个阶段,其中标记和重定位是核心。
1. 初始标记 (Pause Mark Start)
- 任务:从GC Roots(线程栈、全局变量等)开始,标记所有直接可达的对象。这是一个非常快速的、STW的过程。
- 过程:遍历GC Roots,将直接引用的对象压入标记栈中。
2. 并发标记 (Concurrent Mark)
- 任务:从标记栈中的对象开始,递归遍历整个对象图,标记所有存活的对象。
- 状态:完全并发。应用线程仍在运行。
- 关键技术:读屏障。当应用线程加载一个尚未被标记的对象的引用时,读屏障会拦截该引用,并完成对该对象的标记(将其颜色位设为Marked0或Marked1),并将其加入标记栈。这确保了所有在并发标记阶段被访问到的对象都会被正确标记。
3. 最终标记 (Pause Mark End)
- 任务:处理一些边缘情况(如弱引用处理),并确保标记栈是空的,标志着整个对象图的标记工作已经完成。
- 状态:一个非常短暂的 STW 停顿。
4. 并发准备重定位 (Concurrent Prepare for Relocate)
- 任务:根据标记结果(存活对象),筛选出哪些内存区域包含最多的垃圾(即最适合进行压缩回收),并创建重分配集(Relocation Set)。
5. 初始重定位 (Pause Relocate Start)
- 任务:为重分配集中的每个Region分配转发指针(Forwarding Pointer)所需的数据结构。
- 状态:一个非常短暂的 STW 停顿。
6. 并发重定位 (Concurrent Relocate)
- 任务:这是ZGC最核心、最神奇的部分。它将重分配集中的存活对象复制到新的Region中。
- 状态:完全并发。
- 工作原理:
- GC线程开始复制存活对象到新的Region。
- 同时,应用线程仍在运行,并可能访问已被重定位或尚未重定位的对象。
- 读屏障再次发挥关键作用:
- 如果一个应用线程尝试访问一个尚未被重定位的对象,读屏障会先完成这个对象的复制工作,然后更新应用线程手中的引用,使其指向新地址。
- 如果一个应用线程尝试访问一个已经被其他线程重定位的对象,读屏障会通过对象旧地址上的“转发指针”来“治愈”这个引用,直接返回新地址。
- 这个过程确保了所有引用最终都会被正确地更新到新地址,而无需暂停应用线程。
下图展示了ZGC如何通过其并发的标记和重定位阶段,将漫长的STW时间分解为多个极短的停顿,从而实现其超低延迟的目标:
ZGC如何解决“对象消失”问题
与G1的SATB和CMS的增量更新不同,ZGC的并发标记基于一种称为 “指针着色” 的机制,它本身通过读屏障就足以保证标记的正确性。
在ZGC的并发标记阶段:
- 对象的状态通过指针的颜色位来标识(Marked0/Marked1)。
- 当应用线程试图将一个引用写入某个字段时(A.field = B),它只是简单地存储了一个指向B的指针(可能带着未标记的颜色)。
- 当另一个线程(或之后同一个线程)读取 A.field 时,读屏障会被触发。
- 读屏障检查引用B的颜色。如果它尚未被标记,读屏障会立即将其标记(修改颜色位),然后再将“治愈”后的正确引用返回给应用线程。
这种方式确保了:
- 任何被存活对象引用的对象,在引用被读取时都会被立刻标记,从而不会被错误回收。
- 它不需要像G1那样维护复杂的RSet来跟踪跨Region引用,因为“治愈”工作是在读取时即时完成的。
ZGC的优缺点与调优
优点
- 超低停顿:停顿时间通常不超过10ms,且不随堆大小增长而增长。
- 高吞吐量:虽然引入了读屏障,但其设计非常高效,吞吐量损失相对于G1通常只在15%以内。
- 简化的调优:参数远比G1简单,核心目标通常只需设定最大停顿时间(-XX:MaxGCPauseMillis)。
缺点与注意事项
- CPU开销:读屏障虽然高效,但仍会带来额外的CPU开销。在CPU极度紧张的应用中,可能需要评估其影响。
- JDK版本:生产环境强烈建议使用其正式版后的LTS版本,如JDK 17或JDK 21,以获得最佳的稳定性和性能。
- 观察性工具:传统的jstat等工具对ZGC的支持有限,需要依赖ZGC自己的日志和更新的工具(如JDK的jcmd、GC日志分析)来进行监控和调优。
关键参数
- 启用ZGC:-XX:+UseZGC
- 设置最大停顿时间目标:-XX:MaxGCPauseMillis=5 (单位ms,默认无目标,但ZGC会尽力优化)
- 启用并行GC线程动态调整:-XX:+UseDynamicGCThreads (推荐,默认开启)
- 开启GC日志:-Xlog:gc*:gc.log (JDK 9+ Unified Logging格式)
总结
ZGC代表了Java垃圾收集技术的前沿。它通过革命性的着色指针和读屏障技术,将GC停顿时间降低到了一个前所未有的水平,几乎让Java应用程序感觉不到垃圾收集的存在。尽管它需要更高的JDK版本支持,并且可能带来轻微的CPU开销,但对于追求极致响应速度的应用(如金融交易、大数据处理、实时游戏服务器),ZGC是一个改变游戏规则的选择。随着JDK的持续演进,ZGC正变得越来越成熟和稳定,是未来Java大内存低延迟应用的默认选择。