返回文章列表
JVM虚拟机
JVMGCShenandoah低延迟

12新一代的GC

1.垃圾回收器的技术演进

JVM 垃圾回收器经历了多次演进,每代产品都在 STW 时间、内存占用、吞吐量之间做不同的取舍。

早期典型组合包括 PS+PO(Parallel Scavenge + Parallel Old)和 ParNew+CMS。CMS 采用并行标记-并发清理-并发整理的策略,但会产生内存碎片,当无法分配 4 字节空间时会出现两种退化场景:

  1. 触发 FULL GC 并进行整理,此时会产生长时间 STW。
  2. 退化成串行回收,产生长时间的 STW。

GC 演进

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

GC 演进路线

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

设计目标对比

2.Shenandoah GC

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

Shenandoah 概览

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*},选择较高的版本性能更好;如果兼容性有问题(无法启动),选择较低的版本。

Shenandoah 下载

将 OpenJDK 配置到环境变量中,使用 java -version 进行测试,打印出 Shenandooh 相关信息即代表成功。运行 Java 程序时添加如下参数:

  • -XX:+UseShenandoahGC:开启 Shenandoah GC
  • -Xlog:gc:打印 GC 日志

Shenandoah 使用

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 停顿时间

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 支持分代

ZGC 版本更迭

3.2 使用方法

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

  • 分代 ZGC:-XX:+UseZGC -XX:+ZGenerational
  • 非分代 ZGC:-XX:+UseZGC

ZGC 使用

3.3 参数设置

ZGC 在设计上做到了自适应,根据运行情况自动调整参数,让用户手动配置的参数最少化:

  • 自动设置年轻代大小,无需 -Xmn。
  • 自动晋升阈值(复制中存活多少次才搬运到老年代),无需 -XX:TenuringThreshold。
  • JDK17 之后支持自动的并行线程数,无需 -XX:ConcGCThreads。

必须设置的参数:

  • -Xmx 值:最大堆内存大小。这是 ZGC 最重要的一个参数,必须设置。ZGC 在运行过程中会使用一部分内存处理垃圾回收,所以尽量保证堆中有足够的空间,具体值取决于对象分配速度,根据测试情况决定。

可设置的参数:

  • -XX:SoftMaxHeapSize=值:ZGC 会尽量保证堆内存小于该值,在内存靠近这个值时会尽早进行垃圾回收,但依然有可能超过该值。例如 -Xmx5g -XX:SoftMaxHeapSize=4g,ZGC 会尽量保证堆内存小于 4GB,最多不会超过 5GB。

ZGC 参数

可以使用 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 默认没有开启此功能。

操作步骤:

  1. 计算所需页数。Linux x86 架构中大页大小为 2MB,根据所需堆内存大小估算大页数量。比如堆空间需要 16G,预留 2G(JVM 需要额外的一些非堆空间),那么页数就是 18G / 2MB = 9216。

  2. 配置系统的大页池以具有所需的页数(需要 root 权限):

echo 9216 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
  1. 添加参数 -XX:+UseLargePages 启动程序进行测试。

ZGC 调优

4.实战案例:内存不足时的 GC 测试

Java 服务中存在大量软引用的缓存导致内存不足时,可以测试 G1、Shenandoah、ZGC 这三种垃圾回收器在这种场景下的回收情况。

步骤:

  1. 启动程序,添加不同的虚拟机参数进行测试。
  2. 使用 Apache Benchmark 测试工具对本机进行压测。
  3. 生成 GC 日志,使用 GcEasy 进行分析。
  4. 对比压测之后的结果。

实战案例

5.小结

ZGC 和 Shenandoah 设计的目标都是追求较短的停顿时间,两种垃圾回收器在并行回收时都会使用垃圾回收线程占用 CPU 资源,具体使用场景如下:

  1. 在内存足够的情况下,ZGC 垃圾回收表现的效果会更好,停顿时间更短。
  2. 在内存不是特别充足的情况下,Shenandoah GC 表现更好,并行垃圾回收的时间较短,用户请求的执行效率比较高。

场景对比