Skip to content

ZGC

概述

ZGC (The Z Garbage Collector) 是 JDK 11 中引入的一款可扩展、低延迟的垃圾回收器。它的设计目标旨在解决现代应用中大内存(TB 级别)和低延迟(亚毫秒级停顿)的需求。

设计目标

  • 停顿时间不超过 10ms。
  • 停顿时间不会随着堆内存或存活对象大小的增加而增加。
  • 支持从 8MB 到 16TB 的堆大小。
  • 对应用吞吐量的影响小于 15%。

ZGC 非常适用于对响应时间要求极为苛刻的服务,例如金融风控、实时竞价、在线游戏等场景。

对于低延迟服务而言,GC 导致的 STW (Stop-The-World) 是可用性的主要痛点。在 STW 期间,所有应用线程都会暂停,等待 GC 完成,这会直接导致服务响应时间出现毛刺。

以 CMS 和 G1 为例,它们虽然在不断优化停顿时间,但仍存在瓶颈。它们都使用了标记-复制算法,其主要停顿来源如下:

  • CMS (新生代 ParNew): Young GC 过程是完全 STW 的。
  • G1: G1 的 Young GC 和 Mixed GC 都包含标记-复制过程。虽然其并发标记阶段可以与应用线程并行,但其对象转移(复制)阶段是完全 STW 的。当存活对象数量巨大时,这个阶段的耗时会变得很长,成为主要的停顿瓶颈。

ZGC 原理

ZGC 通过对标记-复制算法进行重大改进,实现了几乎所有阶段的并发执行,从根本上解决了 STW 耗时过长的问题。

全并发的 ZGC

ZGC 的 GC 周期中,大部分繁重的工作,如标记、转移(复制)、重定位,都是并发执行的,不会暂停应用线程。其 STW 阶段只有三个,且耗时极短:

  1. 初始标记:仅标记 GC Roots 的直接引用,耗时与 GC Roots 数量成正比,通常在 1ms 以内。
  2. 再标记:处理并发标记期间发生变化的对象,耗时通常也非常短。
  3. 初始转移:同样只处理 GC Roots,耗时稳定且短暂。

由于 ZGC 的停顿时间只和 GC Roots 的数量有关,与堆大小和存活对象数量无关,因此它能够实现可预测的、极低的停顿时间。

ZGC 关键技术

ZGC 实现全并发的核心在于两项关键技术:着色指针 (Colored Pointers) 和读屏障 (Read Barriers)。

1. 着色指针 (Colored Pointers)

ZGC 利用了 64 位操作系统指针中并未完全使用的 bit 位。它将对象地址的特定 bit 位用于存储元数据,如对象的标记状态(Marked)、是否已经重定位(Remapped)等。

  • 优势:GC 线程可以直接通过指针的颜色来判断对象状态,无需访问对象头,速度更快,且减少了内存访问。
  • 地址空间:ZGC 将 64 位虚拟地址划分为不同的视图(如 M0, M1, Remapped),在 GC 的不同阶段,通过切换地址视图来高效地管理对象。例如,一个对象的物理地址是固定的,但它在 GC 过程中可以有多个不同“颜色”的虚拟地址。

2. 读屏障 (Read Barriers)

读屏障是 JVM 在 JIT 编译时插入到代码中的一小段逻辑。当应用程序从堆中读取一个对象引用时,会触发这段逻辑。

  • 作用:在 GC 的并发转移阶段,应用线程可能会读取一个已经被 GC 线程移动了的对象。此时,读屏障会拦截这次读取操作,发现指针指向的是旧地址(通过指针颜色判断),然后它会自我修复,将指针更新为对象的新地址再返回给应用线程。
  • 对应用的影响:这个过程对应用代码是透明的,保证了即使在并发转移过程中,应用线程也总能访问到正确的对象。不过,读屏障会带来一定的性能开销,这也是 ZGC 可能会轻微降低应用吞吐量的一个原因。

ZGC 调优实践

ZGC 并非开箱即用就是最优的,需要根据服务特性进行调优。

重要配置参数

  • -XX:+UseZGC: 启用 ZGC。
  • -Xms, -Xmx: 建议设置为相同的值,避免堆内存动态伸缩带来的开销。
  • -XX:ConcGCThreads: 并发 GC 的线程数。增加该值可以加快 GC 速度,但会占用更多 CPU 资源,可能影响应用吞吐量。
  • -XX:ZCollectionInterval: 设置一个固定的时间间隔(秒)来触发 GC。适用于应对突发流量,避免自适应算法响应不及时。
  • -XX:ZAllocationSpikeTolerance: 自适应算法的修正系数(默认为 2)。值越大,GC 触发得越保守、越频繁,有助于应对内存分配速率突然飙升的情况。
  • -XX:-ZProactive: 关闭 ZGC 的主动回收机制。如果已经设置了固定间隔触发,可以关闭此项以避免不必要的 GC。

GC 触发时机

理解 ZGC 何时触发 GC 至关重要,核心目标是避免因垃圾回收速度跟不上分配速度而导致的“内存分配阻塞 (Allocation Stall)”,这会造成应用线程长时间停顿。

  • 自适应算法:最主要的触发方式,根据近期的内存分配速率和 GC 耗时来动态计算下一次 GC 的触发时机。
  • 固定时间间隔:通过 -XX:ZCollectionInterval 设置,用于兜底,应对突发流量。
  • 内存分配阻塞:最不希望看到的触发方式,表示堆内存已被耗尽,应用线程必须等待 GC 完成才能继续。

调优案例分析

案例一:流量突增导致性能毛刺

  • 现象:日志中出现大量 "Allocation Stall"。
  • 分析:自适应算法没能及时预测到流量高峰,GC 触发过晚。
  • 解决:
    1. 设置 -XX:ZCollectionInterval 来保证规律性的 GC。
    2. 增大 -XX:ZAllocationSpikeTolerance 使自适应算法更敏感。

案例二:GC 频繁但依然出现性能毛刺

  • 现象:GC 间隔很短,但还是出现阻塞。
  • 分析:GC 触发及时,但回收速度跟不上分配速度。
  • 解决:增大 -XX:ConcGCThreads,投入更多 CPU 资源来加快并发回收。

案例三:单次 GC 停顿时间远超预期

  • 现象:STW 阶段日志显示 "Pause Roots ClassLoaderDataGraph" 耗时较长。
  • 分析:STW 时间与 GC Roots 数量正相关。经排查发现系统中存在上万个 ClassLoader 实例,它们都是 GC Roots。
  • 解决:从应用层面解决,例如升级不合理使用 ClassLoader 的组件版本。

案例四:服务运行越久,GC 停顿越长

  • 现象:STW 阶段日志显示 "Pause Roots CodeCache" 耗时随时间增长。
  • 分析:CodeCache(用于存放 JIT 编译后的代码)也是 GC Roots 的一部分。随着服务运行,大量方法被 JIT 编译,导致 CodeCache 膨胀。
  • 解决:从业务层面优化,减少需要 JIT 编译的代码量,例如精简或删除无用逻辑。