新一代垃圾回收器ZGC设计与实现
国内首本系统讲解ZGC垃圾回收器的技术专著。作者彭成寒以10年Java和C++开发经验为基础,从设计理念到源码实现全面拆解Oracle在Java 11引入的新一代GC。全书共9章,覆盖ZGC内存管理、线程模型、并发标记算法、日志解读、参数调优和编译调试。ZGC承诺在TB级堆上实现小于10毫秒的停顿时间,对吞吐量影响低于15%。豆瓣评分8.2。
本书速读
📖 全书概述:ZGC为什么是垃圾回收的范式转变
在理解ZGC之前,需要先理解它要解决的问题。传统的JVM垃圾回收器(包括当时最先进的G1)都面临一个根本性的矛盾:堆越大,GC停顿时间越长。这是因为传统GC在做标记和整理时,需要「Stop-The-World」——暂停所有应用线程,独占CPU来完成回收工作。当堆达到TB级别时,即使是最优化的算法,STW停顿也会达到数百毫秒甚至秒级。
ZGC的目标是直接打破这个矛盾:在TB级堆上,停顿时间依然保持在10毫秒以内,对吞吐量的影响不超过15%。这不是渐进式的优化,而是范式级别的转变。它实现这个目标的核心手段有三项:
并发标记和并发整理:传统GC在标记阶段需要STW,而ZGC让标记和应用线程并发执行。整理(移动对象消除碎片)也同样是并发完成。这意味着GC工作几乎不会阻塞应用线程。
颜色指针(Colored Pointers):这是ZGC最具创新性的设计。它在对象的指针中嵌入状态信息(标记位),用64位指针的高几位来存储对象的GC状态(已标记、未标记、finalizeable、remapped等)。这样GC无需额外的位图来追踪对象状态,状态信息直接编码在指针中,极大地提高了标记效率。
读屏障(Load Barriers):ZGC使用读屏障而非写屏障来实现并发标记。当应用线程读取一个对象引用时,读屏障会拦截这个读取操作,检查被引用对象是否需要被标记或重新定位。如果需要,读屏障会立即处理,保证应用线程看到的永远是正确的对象地址。这个设计是ZGC能够实现并发整理的核心机制。
这三个机制的组合,构成了ZGC区别于所有前代GC的底层架构。理解了这个架构,才能理解ZGC每一个具体设计的由来。
🗺️ 垃圾回收器演进:从Serial到ZGC的技术脉络
作者在第一章通过简明扼要的梳理,建立了垃圾回收器的演进图谱。理解这条脉络,是理解ZGC设计选择的前提。
Serial GC(串行回收):最基础的回收器,单线程完成所有GC工作,STW时间等于整个GC耗时。适用于小堆、单线程场景。它的核心逻辑很简单:暂停一切→回收→恢复。简单但效率极低。
Parallel GC(并行回收):在Serial基础上引入多线程并行回收,减少STW的绝对时间。但本质没变——依然是STW,只是用更多CPU来缩短暂停时间。它的瓶颈在于:当堆很大时,即使多线程也无法把STW压缩到可接受的范围。
Concurrent Mark-Sweep(CMS):首次引入并发标记的概念,让标记阶段和应用线程并发执行,大幅减少STW时间。但它有两个致命缺陷:一是浮动垃圾问题(并发期间产生的新垃圾无法在本次GC中回收),二是内存碎片化(CMS只标记不整理,长期使用后碎片越来越多)。
G1 GC:在CMS基础上引入Region概念,把堆划分为多个等大小的Region,通过预测模型选择回收收益最高的Region集合进行回收。G1的突破在于引入了「可预测的停顿时间」——你可以告诉G1「我希望停顿不超过200毫秒」,它会自动选择合适的Region数量。但G1依然需要在Mixed GC阶段进行STW的整理操作,当堆达到TB级时,这个STW依然无法控制在10毫秒以内。
ZGC:直接跳出了前代GC「STW整理」的思路,实现了全流程并发的垃圾回收。标记和整理都不需要STW(除了极短的初始停顿),这是ZGC与前代GC最根本的区别。它的设计哲学是:不应该让GC停顿时间随堆大小线性增长,停顿时间应该是一个常数,与堆大小无关。
这条演进脉络的逻辑是:串行→并行(多线程缩短STW)→并发标记(减少STW但依然要STW整理)→全并发(ZGC,消除STW整理)。每一代都是在前一代的瓶颈上做突破,ZGC突破的正是「STW整理」这个最后的堡垒。
🧩 颜色指针:ZGC最核心的创新设计
颜色指针是理解ZGC一切其他设计的基础。它解决的问题是:在并发标记的场景下,如何高效地追踪每个对象的标记状态?
为什么需要追踪对象状态:在并发标记中,应用线程和GC线程同时在运行。GC线程在标记对象的同时,应用线程可能创建了新对象、修改了对象引用关系。GC需要知道每个对象当前处于什么状态(是否已标记、是否需要被重新定位等),才能正确地工作。
传统方案的问题:前代GC通常使用独立的位图(Bitmap)来存储对象的标记状态。位图是一个与堆内存平行的数据结构,每个对象对应位图中的一位。这种方式的问题是:每次需要查位图来获取状态,增加了一次间接访问的开销;位图本身也需要管理,增加了复杂性。
ZGC的方案——颜色指针:ZGC把对象的标记状态直接编码在对象的指针中。在64位系统中,指针的高几位(具体是高4位)被用来存储状态信息,剩余的位存储实际的对象地址。这几个位被称为「颜色位」,不同的颜色组合表示不同的状态:Finalizable(对象可被终结)、Remapped(对象已重定位)、Marked1和Marked0(标记位组合表示是否已标记)。
颜色指针的工作机制:当GC开始一轮标记时,它会翻转全局的颜色位(比如从Marked0翻转到Marked1)。然后当应用线程访问对象时,读屏障会检查对象指针的颜色位:如果颜色位是旧的(说明对象还没有被本轮GC标记),读屏障会把这个对象加入标记队列,同时把指针的颜色位翻转到新的颜色。这样,通过指针的颜色位就能直接判断对象的状态,无需查询额外的位图。
多映射地址空间:为了让颜色指针可行,ZGC使用了一种巧妙的内存映射技术。物理内存只有一份,但通过虚拟内存的多重映射,同一份物理内存可以被映射到多个不同的虚拟地址空间,每个空间对应一种颜色组合。这样,当需要翻转颜色位时,GC只需要把对象的引用从一种映射切换到另一种映射,而不需要实际移动对象的物理内存。这是颜色指针能高效工作的基础设施。
📖 读屏障:ZGC实现并发整理的关键
读屏障是ZGC区别于前代GC的另一项核心设计。它解决的问题是:在GC并发移动对象(整理阶段)时,如何保证应用线程读取到的对象地址始终是正确的?
写屏障 vs 读屏障:前代的并发GC(如CMS、G1)主要使用写屏障(Write Barrier)。写屏障在应用线程写入对象引用时触发,记录引用关系的变化,用于增量更新。而ZGC使用读屏障(Load Barrier),在应用线程读取对象引用时触发。这个选择的根本原因是:ZGC需要在整理阶段并发地移动对象,而移动对象后,所有指向旧地址的引用都需要被更新。写屏障无法解决这个问题(因为它只在写入时触发,而整理阶段是GC在移动对象,不是应用在写入),所以必须用读屏障——每次读取引用时,都检查这个引用指向的对象是否被移动过,如果移动过,就自动更新为新的地址。
读屏障的具体工作流程:当应用线程执行一个对象读取操作(比如 obj.field)时,JVM会在实际读取之前插入一段屏障代码。这段代码检查被引用对象的指针颜色位:如果颜色位表明对象已被重定位(Remapped),说明对象的物理地址已经变了,屏障代码会自动将引用更新到新地址,然后返回新地址的对象;如果颜色位表明对象还未被重定位,屏障代码直接返回当前地址。整个过程对应用线程是透明的——它不需要知道自己读到的地址是被屏障更新过的。
读屏障的性能优化:频繁的读屏障会带来性能开销,ZGC做了多层优化。第一层是快速路径(Fast Path)——如果对象颜色位已经是最新状态,屏障几乎零开销,只是一个简单的位检查。第二层是缓存优化——被访问过的对象状态会被缓存,减少重复检查。第三层是JIT编译优化——JIT编译器会把读屏障内联到热路径中,避免函数调用的开销。这些优化使得ZGC对吞吐量的影响控制在15%以内。
💾 ZGC内存管理:Page与Region的设计
ZGC的内存管理建立在Page的概念上。理解Page的设计,是理解ZGC内存分配和回收逻辑的基础。
Page的分类:ZGC把堆内存划分为多种类型的Page。Small Page(默认2MB)用于分配小对象(小于256KB);Medium Page(默认32MB)用于分配中等对象(256KB到4MB之间);Large Page用于分配大对象(大于4MB)。这种分类的逻辑是:不同大小的对象有不同的分配和回收特征,分开管理可以提高效率。小对象数量多、生命周期各异,需要高效的分配和回收策略;大对象数量少、通常生命周期较长,需要不同的处理方式。
Page的状态机:每个Page都有明确的生命周期状态:Unused(未使用)→ Used(正在使用)→ Relocation Set(回收集,等待回收)→ Unused(回收完成后回到未使用)。GC选择需要回收的Page集合(Relocation Set),将其中的存活对象复制到新的Page中,然后释放旧Page。这个过程中,读屏障确保所有对旧Page中对象的引用都能自动指向新Page。
NUMA感知分配:ZGC支持NUMA(Non-Uniform Memory Access)架构下的内存分配优化。在多CPU系统中,每个CPU访问本地内存的速度远快于访问远程内存。ZGC会尽量让对象分配在访问它的线程所在的NUMA节点的本地内存中,从而减少跨节点的内存访问延迟。这是ZGC在大机器上保持高性能的重要优化。
🔄 ZGC垃圾回收算法:并发标记与并发整理的协作
ZGC的回收流程分为几个阶段,每个阶段都精心设计了并发策略,以实现极低的停顿时间。
初始标记(Initial Mark):这是一个极短的STW阶段。GC暂停所有应用线程,标记GC Roots直接引用的对象,然后恢复应用线程。这个阶段的停顿时间非常短(通常1毫秒以内),因为它只需要标记根对象,不需要遍历整个对象图。
并发标记(Concurrent Mark):这是ZGC的核心阶段。GC线程和应用线程并发执行,GC线程从根对象出发,遍历整个对象图,标记所有存活对象。在这个过程中,读屏障确保GC线程能正确追踪应用线程修改的引用关系。这个阶段完全并发,不阻塞应用线程。
初始引用处理(Initial Reference Processing):又一个极短的STW阶段。GC暂停应用线程,处理弱引用(WeakReference、SoftReference、PhantomReference)的清理工作,然后恢复应用线程。
并发整理(Concurrent Relocation):这是ZGC最独特的阶段。GC选择一组Page作为Relocation Set,将其中的存活对象复制到新的Page中。这个过程是完全并发的——GC线程在移动对象的同时,应用线程在正常运行。读屏障确保应用线程读取到的是正确的对象地址(无论对象是否已被移动)。这个阶段消除了传统GC中STW整理的瓶颈。
最终引用处理和恢复(Final Reference Processing & Reset):最后一个极短的STW阶段。完成最终的引用清理工作,重置数据结构,为下一轮GC做准备。
整个流程中,真正STW的阶段只有三个,每个都非常短(通常都在1毫秒以内)。其余阶段全部并发执行。这就是ZGC能将停顿时间控制在10毫秒以内的根本原因。
📊 ZGC日志解读与参数调优
作者在书中专门用章节讲解了如何解读ZGC的运行日志和调优参数,这是将理论转化为实践的关键。
ZGC日志的核心字段:ZGC日志包含了每轮GC的详细信息,包括各个阶段的开始和结束时间、停顿时间、处理的数据量、标记和整理的效率等。关键指标包括:Pause Time(停顿时间)、Mark Duration(标记耗时)、Relocation Duration(整理耗时)、Allocation Rate(分配速率)、Throughput Impact(吞吐量影响)。通过分析这些指标,可以判断ZGC是否在工作在健康状态。
关键参数:-XX:+UseZGC(启用ZGC);-Xmx(设置最大堆大小,ZGC针对大堆设计,建议至少8GB以上);-XX:ZCollectionInterval(设置GC触发间隔);-XX:ZAllocationSpikeTolerance(设置分配尖峰容忍度,用于应对突发的大量对象分配);-XX:+ZUncommit(允许ZGC在未使用内存超过阈值时返还给操作系统)。这些参数大多数情况下不需要调整,ZGC的默认配置已经经过高度优化,但在特定场景下微调可以进一步提升性能。
⭐ 金句与要点
ZGC的设计哲学:GC停顿时间应该是一个常数,与堆大小无关。
颜色指针的本质:把对象的状态编码在指针本身中,消除了对额外位图的依赖。
读屏障的核心价值:让GC可以在不暂停应用线程的情况下安全地移动对象。
ZGC与G1的根本区别:G1的可预测停顿是基于Region选择,而ZGC的可预测停顿是基于全并发架构。
📚 阅读建议
适合有JVM基础、对垃圾回收机制有深入兴趣的Java开发者。建议先了解基本的GC算法(标记-清除、复制、标记-整理)和G1的基本原理,再阅读本书会更容易理解ZGC的创新所在。
阅读方法:重点关注颜色指针、读屏障和并发整理这三个核心机制的设计逻辑。这三者构成了ZGC区别于前代GC的根本架构。理解了这三个机制,其他细节(日志、参数、编译调试)都是在此基础上展开的应用层面内容。
一句话总结:ZGC用颜色指针编码状态、用读屏障保障并发安全、用全并发消除STW整理,在TB级堆上实现了10毫秒以内的停顿时间。