18ZGC和ShenandoahGC原理
随着 Java 应用对低延迟的需求越来越高,G1 已经无法满足一些极致的延迟要求。ZGC 和 Shenandoah 这两款垃圾回收器将 STW 时间压低到了亚毫秒级,是当前最重要的低延迟垃圾回收器。
1.垃圾回收器的技术演进
不同垃圾回收器在设计目标上的取舍各不相同。从 PS+PO、ParNew+CMS、G1,到最新的 ZGC 和 Shenandoah,整体演进趋势是:减少 STW 时间,让更多阶段与用户线程并行执行。

各垃圾回收器的设计目标可以用三维图来表示,分别在吞吐量、内存占用、停顿时间三个维度上进行权衡:

- Parallel GC:吞吐量优先,停顿时间长。
- G1:吞吐量和停顿时间较为均衡。
- Shenandoah:停顿时间短,吞吐量略逊于 G1。
- ZGC:停顿时间最短,吞吐量不佳。
2.ZGC 简介
ZGC 是一种可扩展的低延迟垃圾回收器。ZGC 在垃圾回收过程中,STW 的时间不会超过一毫秒,适合需要低延迟的应用。支持几百兆到 16TB 的堆大小,堆大小对 STW 的时间基本没有影响。

ZGC 降低了停顿时间,能降低接口的最大耗时,提升用户体验。但是吞吐量不佳,所以如果 Java 服务比较关注 QPS(每秒的查询次数)那么 G1 是比较不错的选择。
2.1ZGC 的版本更迭
ZGC 经过多个 JDK 版本的迭代,从实验版本到生产可用,再到支持分代:
| 年份 | JDK 版本 | 重要特性 |
|---|---|---|
| 2018 | JDK11 | 实验版本 |
| 2019 | JDK12-13 | 并行类的卸载、AArch64 架构支持 |
| 2020 | JDK14-15 | Windows & MacOS 支持,第一个正式版本 |
| 2021 | JDK16-17 | 亚毫秒级最大暂停时间,并行线程数的自动计算 |
| 2023 | JDK21 | 支持分代年龄 |

2.2ZGC 的使用方法
OracleJDK 和 OpenJDK 中都支持 ZGC,阿里的 DragonWell 龙井 JDK 也支持 ZGC 但属于其自行对 OpenJDK 11 的 ZGC 进行优化的版本。建议使用 JDK17 之后的版本,延迟较低同时无需手动配置并行线程数。
# 分代 ZGC(JDK21+ 推荐)
-XX:+UseZGC -XX:+ZGenerational
# 非分代 ZGC
-XX:+UseZGC
3.ZGC 原理深入
3.1G1 转移时需要停顿的主要原因
在 G1 垃圾回收器中,STW 时间的主要来源是转移阶段:
- 初始标记,STW,采用三色标记法标记从 GC Root 可直达的对象,STW 时间极短。
- 并发标记,并发执行,对存活对象进行标记。
- 最终标记,STW,处理 SATB 相关的对象标记,STW 时间极短。
- 清理,STW,如果区域中没有任何存活对象就直接清理,STW 时间极短。
- 转移,将存活对象复制到别的区域,STW 时间较长。
在转移时,能不能让用户线程和 GC 线程同时工作呢?问题在于:转移完之后,需要将 A 对象的引用更改为新对象的引用。但是在更改前,如果执行 A.c.count = 2,此时更改的是转移前对象中的属性。更改引用之后,A 引用了转移之后的对象,此时获取 A.c.count 发现属性值依然是 1,这样就产生了问题。所以 G1 为了解决这个问题,在转移过程中需要进行用户线程的停止。
ZGC 和 Shenandoah 解决了这个问题,让转移过程也能够并发执行。
3.2读屏障(Load Barrier)
在 ZGC 中,使用了**读屏障(Load Barrier)**技术来实现转移后对象的获取。当获取一个对象引用时,会触发读后的屏障指令,如果对象指向的不是转移后的对象,用户线程会将引用指向转移后的对象。
工作流程:
- f 变量一开始指向转移前的对象。
- 通过读后屏障指令,判断如果是转移前的对象,就改写指针内容,指向转移后的对象。
- 这样对
f.count进行赋值操作,操作的就是转移后的对象了。
那么 ZGC 是如何判断对象是转移前还是转移后的呢?它主要使用了着色指针(Colored Pointers)。
3.3着色指针(Colored Pointers)
着色指针将原来的 8 字节保存地址的指针拆分成了三部分:
- 最低的 44 位,用于表示对象的地址,所以最多能表示 16TB 的内存空间。
- 中间 4 位是颜色位,每一位只能存放 0 或者 1,并且同一时间只有其中一位是 1。
- 终结位:只能通过终结器访问
- 重映射位(Remap):转移完之后,对象的引用关系已经完成变更
- Marked0 和 Marked1:标记可达对象
- 16 位未使用。
访问对象引用时使用的是对象的地址。在 64 位虚拟机中,是 8 个字节可以表示接近无限的内存空间,所以一般内存中对象的高几位都是 0 没有使用。着色指针就是利用了这多余的几位,存储了状态信息。
正常应用程序使用 8 个字节去进行对象的访问,现在只使用了 44 位,不会产生问题吗?应用程序使用的对象地址只是虚拟内存,操作系统会将虚拟内存转换成物理内存,而 ZGC 通过操作系统更改了这层逻辑,所以不管颜色位变成多少,指针指向的都是同一个对象。
3.4ZPage 内存结构
在 ZGC 中,与 G1 垃圾回收器一样将堆内存划分成很多个区域,这些内存区域被称之为 ZPage。ZPage 分成三类大中小,管控粒度比 G1 更细,这样更容易去控制停顿时间:
- 小区域:2M,只能保存 256KB 内的对象。
- 中区域:32M,保存 256KB – 4M 的对象。
- 大区域:只保存一个大于 4M 的对象。
3.5ZGC 的执行流程
1. 初始标记阶段
标记 GC Roots 引用的对象为存活对象,数量不多,所以停顿时间非常短。初始阶段会标记 GC Roots 直接关联的对象,对引用这些对象的指针上的 Marked0 位标记为 1。
2. 并发标记阶段
遍历所有对象,标记可以到达的每一个对象是否存活。用户线程使用读屏障,如果发现对象没有完成标记也会帮忙进行标记。
3. 并发处理阶段
选择需要转移的 Zpage,并创建转移表,用于记录转移前对象和转移后对象地址。
4. 转移开始阶段
转移 GC Root 直接关联的对象,不转移的对象 Remapped 值设置成 1,避免重复进行判断。
5. 并发转移阶段
将剩余对象转移到新的 ZPage 中,转移之后将两个对象的地址记入转移映射表。转移完之后,转移前的 Zpage 就可以清空了,转移表需要保留下来。
此时,如果用户线程访问对象引用的目标对象,会通过读屏障,将引用进行重置,修改为对转移后对象的引用,同时将 Remap 标记为 1 代表已经重新映射完成。
并发转移阶段结束之后,这一轮的垃圾回收就结束了,但其实并没有完成所有指针的重映射工作,这个工作会放到下一阶段,与下一阶段的标记阶段一起完成(因为都需要遍历整个对象图)。
3.6第二次垃圾回收的初始标记
第二次垃圾回收的初始标记阶段,沿着 GC Root 标记对象。这一次会使用 Marked1,因为 Marked0 是上一次垃圾回收了。这样可以很容易区分出是这一次垃圾回收的标记阶段还是上一次垃圾回收的。
- 如果 Marked0 为 1 代表上一轮的重映射还没有完成,先完成重映射从转移表中找到老对象转移后的新对象,再进行标记。
- 如果 Remap 为 1,只需要进行标记。
最后将转移映射表删除,释放内存空间。
3.7并发问题
如果用户线程在帮忙转移时,GC 线程也发现这个对象需要复制,那么就会去尝试写入转移映射表,如果发现映射表中已经有相同的老对象,直接放弃。
3.8分代 ZGC 的设计
在 JDK21 之后,ZGC 设计了年轻代和老年代,这样可以让大部分对象在年轻代回收,减少老年代的扫描次数,同样可以提升一定的性能。同时,年轻代和老年代的垃圾回收可以并行执行。
分代之后的着色指针将原来的 8 字节保存地址的指针拆分成了三部分:
- 46 位用来表示对象地址,最多可以表示 64TB 的地址空间。
- 中间的 12 位为颜色位。
- 最低 4 位和最高 2 位未使用。
整个分代之后的读写屏障、着色指针的移位使用都变得异常复杂,仅作了解即可。
4.ZGC 参数与调优
4.1参数设置
ZGC 在设计上做到了自适应,根据运行情况自动调整参数,让用户手动配置的参数最少化:
- 自动设置年轻代大小,无需设置
-Xmn参数。 - 自动晋升阈值(复制中存活多少次才搬运到老年代),无需设置
-XX:TenuringThreshold。 - JDK17 之后支持自动的并行线程数,无需设置
-XX:ConcGCThreads。

需要设置的参数:
-Xmx 值:最大堆内存大小。这是 ZGC 最重要的一个参数,必须设置。ZGC 在运行过程中会使用一部分内存用来处理垃圾回收,所以尽量保证堆中有足够的空间。设置多少值取决于对象分配的速度,根据测试情况来决定。
可以设置的参数:
-XX:SoftMaxHeapSize=值:ZGC 会尽量保证堆内存小于该值,这样在内存靠近这个值时会尽早地进行垃圾回收,但是依然有可能会超过该值。例如-Xmx5g -XX:SoftMaxHeapSize=4g,ZGC 会尽量保证堆内存小于 4GB,最多不会超过 5GB。

4.2ZGC 调优
ZGC 中可以使用 Linux 的 Huge Page 大页技术优化性能,提升吞吐量、降低延迟。注意:安装过程需要 root 权限,所以 ZGC 默认没有开启此功能。
操作步骤:
- 计算所需页数:Linux x86 架构中大页大小为 2MB,根据所需堆内存的大小估算大页数量。比如堆空间需要 16G,预留 2G(JVM 需要额外的一些非堆空间),那么页数就是
18G / 2MB = 9216。 - 配置系统的大页池(需要 root 权限):
echo 9216 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages- 添加参数启动程序进行测试:
-XX:+UseLargePages
5.Shenandoah GC 简介
Shenandoah 是由 Red Hat 开发的一款低延迟的垃圾收集器,Shenandoah 并发执行大部分 GC 工作,包括并发的整理,堆大小对 STW 的时间基本没有影响。

Shenandoah GC 和 ZGC 不同,Shenandoah GC 很多是使用了 G1 源代码改造而成,所以在很多算法、数据结构的定义上与 G1 十分相像,而 ZGC 是完全重新开发的一套内容。
Shenandoah GC 的主要特点:
- Shenandoah GC 的区域定义与 G1 是一样的。
- 没有着色指针,通过修改对象头的设计来完成并发转移过程的实现。
- Shenandoah GC 有两个版本,1.0 版本存在于 JDK8 和 JDK11 中,后续的 JDK 版本中均使用 2.0 版本。
5.1Shenandoah 的使用方法
Shenandoah 只包含在 OpenJDK 中,默认不包含在内需要单独构建,可以直接下载构建好的。下载地址:https://builds.shipilev.net/openjdk-jdk-shenandoah/。
下载时需要选择对应的配置:
- 架构(
{aarch64, arm32-hflt, mipsel, mips64el, ppc64le, s390x, x86_32, x86_64}):使用arch命令选择对应的架构。 - 虚拟机类型(
{server, zero}):选择 server,包含所有 GC 的功能。 - 优化级别(
{release, fastdebug, Slowdebug, optimization}):不同的优化级别,选择 release 性能最高。 - 编译器版本(
{gcc*-glibc*, msvc*}):选择较高的版本性能好一些,如果兼容性有问题(无法启动),选择较低的版本。
配置完成后将 OpenJDK 配置到环境变量中,使用 java -version 进行测试。打印出 Shenandoah 相关信息代表成功。
# 开启 Shenandoah GC
-XX:+UseShenandoahGC
# 打印 GC 日志
-Xlog:gc
5.2ShenandoahGC 1.0 版本
如果转移阶段未完成,此时转移前的对象和转移后的对象都会存活。如果用户去访问数据,需要使用转移后的数据。Shenandoah GC 1.0 使用了读前屏障,根据对象的前向指针来获取到转移后的对象并读取。
写入数据时会使用写前屏障,判断 Mark Word 中的 GC 状态:
- 如果 GC 状态为 0 证明没有处于 GC 过程中,直接写入。
- 如果不为 0 则根据 GC 状态值确认当前处于垃圾回收的哪个阶段,让用户线程执行垃圾回收相关的任务。
1.0 版本的缺点:
- 对象内存大大增加:每个对象都需要增加 8 个字节的前向指针,基本上会占用 5% - 10% 的空间。
- 读屏障中加入了复杂的指令,影响使用效率。
5.3ShenandoahGC 2.0 版本
2.0 版本优化了前向指针的位置,仅转移阶段将其放入了 Mark Word 中,避免了 1.0 版本中每个对象都要长期占用 8 字节空间的问题。
5.4ShenandoahGC 的并发转移
Shenandoah GC 的执行流程与 ZGC 类似,包含初始标记、并发标记、最终标记、并发清理、并发转移等阶段。在并发转移阶段,如果用户线程在帮忙转移时,Shenandoah GC 线程也发现这个对象需要复制,那么就会去尝试写入前向指针,使用了类似 CAS 的方式来实现,只有一个线程能成功修改,其他线程会放弃转移的操作。
6.ZGC 与 Shenandoah 的实战对比
实战案例:内存不足时的垃圾回收测试。需求:Java 服务中存在大量软引用的缓存导致内存不足,测试下 G1、Shenandoah、ZGC 这三种垃圾回收器在这种场景下的回收情况。

步骤:
- 启动程序,添加不同的虚拟机参数进行测试。
- 使用 Apache Benchmark 测试工具对本机进行压测。
- 生成 GC 日志,使用 GcEasy 进行分析。
- 对比压测之后的结果。
6.1ZGC 和 Shenandoah 的使用场景
ZGC 和 Shenandoah 设计的目标都是追求较短的停顿时间,它们具体的使用场景如下:

两种垃圾回收器在并行回收时都会使用垃圾回收线程占用 CPU 资源:
- 在内存足够的情况下,ZGC 垃圾回收表现的效果会更好,停顿时间更短。
- 在内存不是特别充足的情况下,Shenandoah GC 表现更好,并行垃圾回收的时间较短,用户请求的执行效率比较高。
6.2如何选择
| 场景 | 推荐 GC | 原因 |
|---|---|---|
| 关注 QPS、吞吐量 | G1 | ZGC 吞吐量不佳 |
| 内存充足、追求极致低延迟 | ZGC | 停顿时间最短,亚毫秒级 |
| 内存不是特别充足、需要低延迟 | Shenandoah | 并行回收时间较短,用户请求执行效率高 |
| 巨大堆(超过 6G)+ 关注停顿 | G1 或 ZGC | 都支持大堆,ZGC 堆大小对 STW 几乎无影响 |
总体而言,ZGC 和 Shenandoah 是 Java 在低延迟方向上的重要探索。ZGC 通过着色指针和读屏障实现了亚毫秒级 STW,Shenandoah 通过对象头前向指针实现了并发转移,两者各有侧重,需要根据应用场景选择。