低延迟垃圾收集器
ZGC 与 Shenandoah 是通过并发标记和移动对象来缩短停顿的垃圾收集器。本文先说明对象移动时应用怎样继续访问,再介绍 Java 17、21 的配置与分配余量。最后讨论 CPU、吞吐和内存代价,以及迁移时怎样验证业务效果。
1. 面试核心
先记住这几条,再读正文- 并发移动。 ZGC 与 Shenandoah 让应用在对象移动期间继续运行,通过引用屏障找到对象的正确位置。
- Java 21 基线。 Java 21 的 ZGC 默认不分代,
-XX:+ZGenerational开启分代。 - 分代 ZGC 屏障。 分代 ZGC 用加载屏障修正对象移动后的引用,用存储屏障处理标记并记录跨代引用。卡 · 加载屏障与存储屏障
- 与 G1 的差异。 G1 的主要疏散在 STW 中完成,ZGC 并发重定位对象。卡 · 并发重定位
- 分配停滞。 并发回收跟不上分配时,ZGC 的分配线程会等待可用空间。卡 · 分配余量
2. 并发重定位
对象重定位指把存活对象移动到新的内存位置,以腾出可回收的空间。G1 的主要疏散工作在应用暂停期间完成,存活对象越多,需要搬运的工作就越多。低延迟收集器则让应用在移动期间继续访问对象,把主要耗时工作移到并发阶段。
这时,应用持有的引用还会暂时指向旧位置。引用屏障是在读写引用时插入的检查与处理逻辑,它会帮助应用找到移动后的正确位置。ZGC 用染色指针记录元数据,也就是在引用中附带标记等状态。Shenandoah 则结合转发信息与引用屏障找到新位置,转发信息记录对象搬到了哪里。
并发执行改变了工作发生的时机,也带来以下资源成本:
- 应用读写引用时,需要执行屏障检查。
- GC 并发线程消耗 CPU 和内存带宽,与业务计算共享资源。
- 堆要为回收期间的新分配和对象重定位预留空间。
- 某些阶段仍要暂停应用,协调根处理或阶段切换。这类全体应用线程暂停称为 Stop-The-World(STW)。
因此,主要搬运工作可以与应用并发,短暂的协调暂停仍然存在。
3. 分代 ZGC
分代 ZGC 将年轻对象与长寿命对象分开回收,但是否启用分代取决于 JDK 版本。Java 17、21 的配置如下:
| JDK 版本 | ZGC 模式 |
|---|---|
| 17 | -XX:+UseZGC 启用非分代 ZGC。 |
| 21 | -XX:+UseZGC 默认不分代,另加 -XX:+ZGenerational 启用分代。 |
Java 21 启用分代 ZGC 的命令如下:
java -XX:+UseZGC -XX:+ZGenerational -jar app.jar
新创建的对象中,有很多很快就失去引用。分代 ZGC 可以更频繁地回收年轻代,把长寿命对象留在老年代,因此减少了反复扫描老对象的工作。ZGC 可用于不同大小的堆,是否适合仍需按分配模式和资源预算验证。
3.1 加载屏障与存储屏障
分代 ZGC 既要处理对象移动,也要跟踪老年代指向年轻代的引用。加载屏障和存储屏障分别在读取与写入引用时完成这些工作。
加载屏障(load barrier)检查引用中的位置元数据。对象已经移动时,它取得正确地址,让应用继续访问同一个对象。
存储屏障(store barrier)在引用字段写入时执行,承担标记工作,并记录需要追踪的跨代引用。这样,年轻代回收就能找到老年代对年轻代对象的引用,避免为了找这些引用而扫描全部老对象。
染色指针保存不同阶段的状态,供屏障快速判断是否需要进一步处理。因此,分代模式需要同时理解加载屏障和存储屏障。
4. Shenandoah
Shenandoah 也通过并发标记和移动对象缩短停顿,由 OpenJDK 社区开发。目标 JDK 是否包含它,取决于发行版、平台与具体构建。Oracle JDK 与某些 OpenJDK 发行版提供的收集器也有差异,所以需要核对实际安装的构建。
Java 17、21 的 Shenandoah 使用非分代回收,默认模式为 satb。SATB 指“开始时快照”,通过记录引用变化,保留标记开始时可达的对象。支持的构建可用以下命令启用:
java -XX:+UseShenandoahGC -jar app.jar
启动失败时,应核对构建支持与具体错误。如果所用 JVM 无法识别 UseShenandoahGC,就需要确认当前构建是否提供该收集器。仅凭 Java 主版本号还无法判断。
5. 分配余量
并发 GC 期间,应用仍在创建对象,因此堆需要支撑回收完成前的新分配。设分配率为 A,也就是单位时间分配的字节数,一次并发周期耗时为 T。这段时间的新分配量约为 A × T,估算假设分配率稳定,且暂未回收空间。
存活工作集(live set)指回收后仍需保留的对象集合。除了这部分对象,堆还要容纳新分配和重定位,因此需要满足以下容量与资源条件:
- 最大堆应在稳定存活工作集之外,为并发周期内的新对象留出空间。
- GC 并发线程需要足够 CPU 配额,否则周期变长,
A × T也会增大。 - 流量突发和巨型对象会突然消耗更多空间,需要控制突发分配并保留波动余量。
- 并发周期需要及时启动,让回收能在剩余空间耗尽前完成。
如果堆只比存活工作集略大,应用很快就会耗尽剩余空间,并发回收也就失去了继续运行的余量。
ZGC 缺少可分配空间时,会出现 allocation stall,也就是分配线程等待 GC 腾出空间。Shenandoah 则可进入 Degenerated GC,在 STW 中继续当前周期。空间仍不足时,它还可执行 Full GC。因此,两者的失败路径要分别根据日志判断。
6. 吞吐、CPU 与内存的代价
低停顿来自把工作移到并发阶段,这些工作仍会消耗资源。引用屏障和 GC 线程与业务代码竞争 CPU、内存带宽,堆还需要额外余量。对象布局、压缩指针支持与平台限制也会随收集器和版本变化。
因此,与 G1 或 Parallel 比较时,要在相同负载下测量以下业务与资源指标:
- 吞吐量反映单位时间完成了多少请求。
- P99、P999 分别表示 99%、99.9% 的请求延迟低于该值,用来观察长尾。
- GC CPU 反映回收占用了多少处理器资源。
- RSS 是进程当前驻留在物理内存中的大小,用来检查容器内存压力。
- 分配停滞反映应用是否因等待可用空间而停下。
短 GC pause 与高请求延迟可以同时出现。CPU 饱和、应用锁、I/O、限流和缺页都会增加请求延迟。缺页指访问的页面尚未就绪,需要操作系统补齐映射或读取数据。因此,GC 暂停和端到端请求延迟要分别观察。
6.1 软堆上限与最大堆
ZGC 可以在日常运行中尽量控制堆大小,同时为分配突发保留更高的硬上限。下面用软目标和最大堆分别表达这两个要求:
java -XX:+UseZGC -Xmx5g -XX:SoftMaxHeapSize=4g -jar app.jar
这项配置让 ZGC 尽量以 4 GiB 作为堆大小目标。为避免分配停滞,它仍可使用额外空间,最多扩展到 -Xmx 指定的 5 GiB。也就是说,软目标保留了应对分配压力的调整空间,硬上限决定最多能使用多少堆。
容器预算仍需覆盖堆外内存。SoftMaxHeapSize 并非进程内存上限。
7. 收集器的选择
收集器选择需要把暂停目标与资源预算放在一起看。可以从以下四类候选开始比较:
- G1 可作为通用服务的基线,在吞吐、内存占用和暂停之间取舍。如果业务允许数百毫秒以内的暂停,可以从 G1 开始验证实际分布。
- ZGC 适合评估严格暂停目标或大堆场景,同时需要验证并发回收成本。
- Shenandoah 是另一种低停顿选择,前提是目标发行版和平台支持。
- Parallel 可用于吞吐优先、能够接受集中暂停的批处理任务。
G1 已满足业务暂停目标时,可以继续依据吞吐、内存占用和现有运维经验使用它。比较其他收集器时,应保持相同 JDK、容器限制和请求回放,用端到端效果判断收益。
8. 迁移验证
迁移验证要确认收集器改善了业务指标,并且在内存紧张时仍能满足要求。验证时保持可比较的负载与资源限制:
- 记录原配置的存活工作集、分配率、暂停分布和 GC CPU,作为比较基线。
- 确认目标构建、平台与监控工具支持,清理旧收集器专用参数。先只切换收集器,并保留简单的堆配置。
- 在相同资源限制下,分别压测稳定流量、突发流量和内存逼近上限的情况。
- 检查 allocation stall、退化回收和 GC CPU,确认回收是否及时提供可用空间。
- 对比端到端请求长尾、吞吐和容器 RSS,判断短暂停顿是否转化为业务收益。
GC 日志与 JFR 的时间线分析见 GC 停顿分析。
9. 常见问题
低停顿描述的是收集器的目标,容量和业务延迟仍受实际负载影响。下面说明这几个边界。
9.1 停顿目标与实时保证
ZGC 的目标是把主要工作并发执行,避免暂停随堆大小线性增长。实际暂停仍受根集合、线程数量、系统调度和实现细节影响,资源争抢还会增加业务延迟。因此,低停顿目标不构成硬实时上限。
9.2 低延迟收集器的容量
使用低延迟收集器后,仍需要为存活工作集、并发新分配和波动余量设置堆容量。堆过小会让分配线程等待回收,过大又会占用本可留给其他进程的资源。配置时还要用 RSS 检查整个进程是否符合容器预算。
9.3 ZGC 与 Shenandoah 的性能比较
ZGC 与 Shenandoah 的性能顺序取决于工作负载。JDK 版本、对象之间的引用关系、分配率、CPU、堆大小和发行版实现都会影响结果。因此,应在目标环境比较吞吐、请求长尾与资源占用。
10 · 面试题卡片
10.1 G1 和 ZGC 的主要差异是什么,为什么不少服务仍使用 G1
出现公司:某云原生企业
考察重点:解释 G1 的暂停疏散与 ZGC 的并发重定位,并结合吞吐、内存和运维经验说明选择依据。
参考回答
G1 的主要对象疏散在 STW 中完成,ZGC 则并发移动对象;G1 已满足暂停目标时,服务可以继续使用它。
G1 能并发标记,但把存活对象搬出回收区域的主要工作仍需暂停应用。ZGC 用引用屏障帮助应用在移动期间找到正确位置,因此把更多工作移出了暂停阶段。代价是屏障、并发 GC 的资源消耗,以及回收所需的堆余量。
如果 G1 已满足业务延迟目标,吞吐、内存占用和现有运维经验都可以成为保留它的理由。迁移应在同一环境比较端到端长尾、吞吐和 RSS,再判断收益。
10.2 低延迟收集器为什么仍可能发生分配停滞
出现公司:阿里云
考察重点:说明存活工作集、分配率、周期时间与堆余量的关系,以及 CPU 不足怎样加大分配压力。
相关内容:分配余量。
参考回答
因为应用在并发回收期间仍会分配对象,回收完成前若耗尽可用空间,分配线程就需要等待。
存活工作集大时,堆中可腾出的空间有限。高分配率会更快消耗空闲空间,CPU 不足或周期启动太晚又会推迟回收完成。因此,堆要为一个并发周期内的新分配、对象重定位和流量波动留出余量。
ZGC 用 allocation stall 表示这类分配等待。诊断时应同时比较堆余量、GC CPU 和周期时长;Shenandoah 的退化回收则要按它自己的日志判断。
10.3 分代 ZGC 的加载屏障和存储屏障分别负责什么?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:解释加载屏障怎样修正重定位引用,存储屏障怎样支持标记与跨代引用追踪。
相关内容:加载屏障与存储屏障。
参考回答
加载屏障负责取得对象移动后的正确地址,存储屏障负责标记与跨代引用记录。
应用读取引用时,加载屏障根据引用元数据判断对象是否已经重定位。应用写入引用字段时,存储屏障完成相应标记,并记录需要追踪的跨代引用。年轻代回收据此找到老年代指向年轻代对象的引用。染色指针保存阶段状态,帮助两类屏障判断是否需要进一步处理。
10.4 ZGC 的 SoftMaxHeapSize 与 Xmx 有什么区别?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:区分 ZGC 的软堆目标与最大堆,并说明容器为什么还需为堆外内存留余量。
相关内容:软堆上限与最大堆。
参考回答
SoftMaxHeapSize 是 ZGC 尽量遵守的软目标,-Xmx 才是堆的硬上限。
配置 4 GiB 软目标、5 GiB 最大堆时,ZGC 会尽量以 4 GiB 为目标运行。为避免分配停滞,它仍可使用额外空间,最多扩展到 5 GiB。进程还包含线程栈、元空间等堆外内存,因此容器容量需要在最大堆之外继续预留。