12新一代的GC
1.垃圾回收器的技术演进
JVM 垃圾回收器经历了多次演进,每代产品都在 STW 时间、内存占用、吞吐量之间做不同的取舍。
早期典型组合包括 PS+PO(Parallel Scavenge + Parallel Old)和 ParNew+CMS。CMS 采用并行标记-并发清理-并发整理的策略,但会产生内存碎片,当无法分配 4 字节空间时会出现两种退化场景:
- 触发 FULL GC 并进行整理,此时会产生长时间 STW。
- 退化成串行回收,产生长时间的 STW。

后续演进路径为:PS+PO → ParNew+CMS → G1 → Shenandoah / ZGC(JDK21 起支持分代)。Shenandoah 和 ZGC 通过并发标记 + 并发复制-整理的方式,将大部分 GC 工作并行化,最大限度降低 STW。

不同的垃圾回收器设计目标差异明显,可从吞吐量、内存占用、停顿时间三个维度对比:Parallel GC 偏吞吐量、低内存占用;G1 居中;Shenandoah 与 ZGC 偏低延迟,其中 ZGC 停顿时间最短。

2.Shenandoah GC
Shenandoah 是由 Red Hat 开发的一款低延迟垃圾收集器,并发执行大部分 GC 工作,包括并发的整理,堆大小对 STW 的时间基本没有影响。

2.1 下载与配置
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 进行测试,打印出 Shenandooh 相关信息即代表成功。运行 Java 程序时添加如下参数:
-XX:+UseShenandoahGC:开启 Shenandoah GC-Xlog:gc:打印 GC 日志

2.2 性能测试
使用 JMH 可以对比 SerialGC、ParallelGC、G1、Shenandoah 在不同对象大小下的表现。核心测试代码如下:
//执行5轮预热,每次持续2秒
@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Benchmark)
public class MyBenchmark {
//每次测试对象大小 4KB和4MB
@Param({"4", "4096"})
int perSize;
private void test(Blackhole blackhole) {
//每次循环创建堆内存60%对象
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
long heapSize = (long) ((heapMemoryUsage.getMax() - heapMemoryUsage.getUsed()) * 0.6);
long size = heapSize / (1024 * perSize);
for (int i = 0; i < 4; i++) {
List<byte[]> objects = new ArrayList<>((int) size);
for (int j = 0; j < size; j++) {
objects.add(new byte[1024 * perSize]);
}
blackhole.consume(objects);
}
}
@Benchmark
@Fork(value = 1, jvmArgsAppend = {"-Xms4g", "-Xmx4g", "-XX:+UseSerialGC"})
public void serialGC(Blackhole blackhole) { test(blackhole); }
@Benchmark
@Fork(value = 1, jvmArgsAppend = {"-Xms4g", "-Xmx4g", "-XX:+UseParallelGC"})
public void parallelGC(Blackhole blackhole) { test(blackhole); }
@Benchmark
@Fork(value = 1, jvmArgsAppend = {"-Xms4g", "-Xmx4g"})
public void g1(Blackhole blackhole) { test(blackhole); }
@Benchmark
@Fork(value = 1, jvmArgsAppend = {"-Xms4g", "-Xmx4g", "-XX:+UseShenandoahGC"})
public void shenandoahGC(Blackhole blackhole) { test(blackhole); }
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}测试结论:Shenandoah GC 对小对象的 GC 停顿很短,但是大对象效果不佳。
3.ZGC
ZGC 是一种可扩展的低延迟垃圾回收器。ZGC 在垃圾回收过程中 STW 的时间不会超过一毫秒,适合需要低延迟的应用。支持几百兆到 16TB 的堆大小,堆大小对 STW 的时间基本没有影响。

ZGC 降低了停顿时间,能降低接口的最大耗时、提升用户体验;但吞吐量不佳,所以如果 Java 服务比较关注 QPS(每秒查询次数),G1 是比较不错的选择。
3.1 版本更迭
ZGC 自 JDK 11 引入以来不断完善:
| 时间 | JDK 版本 | 关键特性 |
|---|---|---|
| 2018 | JDK 11 | 实验版本 |
| 2019 | JDK 12-13 | 并行类的卸载、AArch64 架构支持 |
| 2020 | JDK 14-15 | Windows & MacOS 支持、第一个正式版本 |
| 2021 | JDK 16-17 | 亚毫秒级最大暂停时间、并行线程数自动计算 |
| 2023 | JDK 21 | 支持分代 |

3.2 使用方法
OracleJDK 和 OpenJDK 中都支持 ZGC,阿里的 DragonWell 龙井 JDK 也支持 ZGC,但属于其自行对 OpenJDK 11 的 ZGC 进行优化的版本。建议使用 JDK17 之后的版本,延迟较低且无需手动配置并行线程数。
- 分代 ZGC:
-XX:+UseZGC -XX:+ZGenerational - 非分代 ZGC:
-XX:+UseZGC

3.3 参数设置
ZGC 在设计上做到了自适应,根据运行情况自动调整参数,让用户手动配置的参数最少化:
- 自动设置年轻代大小,无需
-Xmn。 - 自动晋升阈值(复制中存活多少次才搬运到老年代),无需
-XX:TenuringThreshold。 - JDK17 之后支持自动的并行线程数,无需
-XX:ConcGCThreads。
必须设置的参数:
-Xmx 值:最大堆内存大小。这是 ZGC 最重要的一个参数,必须设置。ZGC 在运行过程中会使用一部分内存处理垃圾回收,所以尽量保证堆中有足够的空间,具体值取决于对象分配速度,根据测试情况决定。
可设置的参数:
-XX:SoftMaxHeapSize=值:ZGC 会尽量保证堆内存小于该值,在内存靠近这个值时会尽早进行垃圾回收,但依然有可能超过该值。例如-Xmx5g -XX:SoftMaxHeapSize=4g,ZGC 会尽量保证堆内存小于 4GB,最多不会超过 5GB。

可以使用 JMH 测试 ZGC 与分代 ZGC 的表现:
@Benchmark
@Fork(value = 1, jvmArgsAppend = {"-Xms4g", "-Xmx4g", "-XX:+UseZGC", "-XX:+UseLargePages"})
public void zGC(Blackhole blackhole) { test(blackhole); }
@Benchmark
@Fork(value = 1, jvmArgsAppend = {"-Xms4g", "-Xmx4g", "-XX:+UseZGC", "-XX:+ZGenerational", "-XX:+UseLargePages"})
public void zGCGenerational(Blackhole blackhole) { test(blackhole); }ZGC 整体表现非常不错,分代也让 ZGC 的停顿时间有更好的表现。
3.4 调优:Huge Page 大页技术
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启动程序进行测试。

4.实战案例:内存不足时的 GC 测试
Java 服务中存在大量软引用的缓存导致内存不足时,可以测试 G1、Shenandoah、ZGC 这三种垃圾回收器在这种场景下的回收情况。
步骤:
- 启动程序,添加不同的虚拟机参数进行测试。
- 使用 Apache Benchmark 测试工具对本机进行压测。
- 生成 GC 日志,使用 GcEasy 进行分析。
- 对比压测之后的结果。

5.小结
ZGC 和 Shenandoah 设计的目标都是追求较短的停顿时间,两种垃圾回收器在并行回收时都会使用垃圾回收线程占用 CPU 资源,具体使用场景如下:
- 在内存足够的情况下,ZGC 垃圾回收表现的效果会更好,停顿时间更短。
- 在内存不是特别充足的情况下,Shenandoah GC 表现更好,并行垃圾回收的时间较短,用户请求的执行效率比较高。
