14、JDK 25 新特性:分代 Shenandoah(JEP 521)垃圾收集器优化

Shenandoah 是个低延迟的并发垃圾收集器,从 JDK 12 开始就有了。它的特点是暂停时间短,不管堆大小多少,暂停时间都在 1-10 毫秒左右,这对那些对延迟敏感的应用特别有用。但之前的 Shenandoah 是单代模式,所有对象都混在一起,没有区分年轻对象和年老对象,收集效率可能不够高。

JDK 25 里的 JEP 521 把分代模式从实验性特性提升为正式的生产特性了。分代模式利用了"大多数对象都是短命的"这个观察,把堆分成年轻代和年老代,年轻对象回收更频繁,年老对象扫描更少,这样能提高吞吐量和内存利用率。以前用分代模式需要加 -XX:+UnlockExperimentalVMOptions 参数,现在不用了,直接用 -XX:ShenandoahGCMode=generational 就行。

这个改进对需要低延迟和高吞吐量的应用特别有用,比如实时系统、游戏服务器、交易系统等。分代模式能更高效地回收短命对象,减少对长命对象的扫描,整体性能会更好。虽然默认还是单代模式,但分代模式现在已经是生产就绪的了,可以放心用。

Shenandoah GC 的基本原理

先说说 Shenandoah GC 是咋工作的吧。Shenandoah 是个并发垃圾收集器,它的核心思想是在应用运行的时候同时做垃圾收集,尽量减少暂停时间。

Shenandoah 把堆分成很多区域(Region),每个区域可以独立收集。收集过程包括标记、疏散、压缩等步骤,这些步骤大部分都是并发执行的,应用不需要停下来等。只有在一些关键点,比如开始标记、结束标记等,需要短暂的暂停,这个暂停时间很短,一般在 1-10 毫秒左右。

Shenandoah 用的是"快照在开始"(Snapshot-At-The-Beginning,SATB)算法,在开始标记的时候拍个快照,记录所有活对象,然后并发标记这些对象。标记完成后,把活对象疏散到其他区域,原来的区域就可以回收了。

单代模式的问题是所有对象都混在一起,没有区分年轻对象和年老对象。年轻对象通常很快就死了,应该频繁回收;年老对象通常活很久,不需要频繁扫描。但单代模式对所有对象一视同仁,可能对年轻对象回收不够频繁,对年老对象扫描又太频繁,效率不够高。

分代模式的优势

分代模式就是为了解决这个问题而设计的。它把堆分成年轻代和年老代,年轻代放新对象,年老代放长命对象。这样有几个优势:

第一个优势是回收效率更高。年轻对象通常很快就死了,分代模式可以频繁回收年轻代,快速释放内存。年老对象通常活很久,不需要频繁扫描,可以减少扫描开销。

第二个优势是吞吐量更高。因为年轻代回收更频繁,所以能更快地释放内存,应用可以分配更多对象,吞吐量会提高。年老代扫描更少,CPU 开销更小,整体吞吐量会更好。

第三个优势是内存利用率更高。因为能快速回收短命对象,所以内存利用率会更高,不需要预留太多内存给短命对象。年老代的对象通常都是有用的,不需要频繁回收,内存利用率也会更好。

第四个优势是暂停时间更短。虽然单代模式的暂停时间已经很短了,但分代模式可以进一步优化,年轻代回收可以更频繁,暂停时间可以更短。

启用分代模式

启用分代模式很简单,不需要解锁实验性选项了,直接用命令行参数就行。

# 启用 Shenandoah GC 并设置分代模式
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational MyApp

这个命令会启用 Shenandoah GC,并设置成分代模式。-XX:+UseShenandoahGC 用来启用 Shenandoah GC,-XX:ShenandoahGCMode=generational 用来设置成分代模式。

如果你想用单代模式(默认模式),可以这样:

# 使用单代模式(默认)
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=satb MyApp

或者直接不指定模式,默认就是单代模式:

# 使用默认的单代模式
java -XX:+UseShenandoahGC MyApp

分代模式的配置

分代模式有一些配置参数,可以调整年轻代和年老代的大小、回收频率等。

# 设置年轻代大小(占堆的百分比)
java -XX:+UseShenandoahGC \
     -XX:ShenandoahGCMode=generational \
     -XX:ShenandoahGCHeuristics=adaptive \
     MyApp

分代模式目前只支持 adaptive 启发式算法,这是默认值,不需要手动设置。adaptive 算法会根据应用的行为自动调整回收策略,比如什么时候开始回收、每次回收多少、启用哪些特性等。

年轻代的大小是自动调整的,不需要手动设置。Shenandoah 会根据应用的行为自动调整年轻代大小,确保回收效率最高。一般来说,年轻代占堆的 25%-50% 比较合适,太小会导致频繁回收,太大会导致回收不够频繁。

在代码中监控 GC

你可以在代码里监控 GC 的行为,看看分代模式的效果。

import java.lang.management.*;
import java.util.List;

public class MonitorGenerationalGC {
    public static void main(String[] args) throws Exception {
        // 获取内存管理器,用来监控 GC
        List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();  // 获取所有 GC 管理器
        
        // 打印 GC 信息
        for (GarbageCollectorMXBean gcBean : gcBeans) {  // 遍历所有 GC 管理器
            System.out.println("GC 名称: " + gcBean.getName());  // 打印 GC 名称,比如 "Shenandoah Cycles"
            System.out.println("回收次数: " + gcBean.getCollectionCount());  // 打印回收次数,看看回收了多少次
            System.out.println("回收时间: " + gcBean.getCollectionTime() + " ms");  // 打印回收时间,单位是毫秒
        }
        
        // 获取内存使用情况
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();  // 获取内存管理器
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();  // 获取堆内存使用情况
        
        System.out.println("堆内存使用: " + heapUsage.getUsed() / 1024 / 1024 + " MB / " + 
                          heapUsage.getMax() / 1024 / 1024 + " MB");  // 打印堆内存使用情况,单位是 MB
        
        // 模拟一些工作,产生垃圾
        for (int i = 0; i < 1000000; i++) {  // 循环创建对象,产生垃圾
            new Object();  // 创建对象,这些对象很快就会被回收
        }
        
        // 再次打印 GC 信息,看看回收情况
        System.out.println("\n回收后的 GC 信息:");  // 打印标题
        for (GarbageCollectorMXBean gcBean : gcBeans) {  // 遍历所有 GC 管理器
            System.out.println("GC 名称: " + gcBean.getName());  // 打印 GC 名称
            System.out.println("回收次数: " + gcBean.getCollectionCount());  // 打印回收次数,应该增加了
            System.out.println("回收时间: " + gcBean.getCollectionTime() + " ms");  // 打印回收时间
        }
    }
}

这个例子展示了怎么在代码里监控 GC 的行为。通过监控,你能看到分代模式的回收次数、回收时间等信息,从而了解 GC 的工作情况。

分代模式的工作流程

分代模式是怎么工作的呢?简单说就是年轻代和年老代分开管理,年轻代频繁回收,年老代少回收。

年轻代回收(Young GC)是分代模式的核心。当年轻代满了,Shenandoah 会触发年轻代回收,只回收年轻代的对象。年轻代回收是并发的,应用不需要停下来,可以继续运行。回收完成后,活对象会被提升到年老代,死对象会被回收。

年老代回收(Old GC)是当年老代满了或者需要回收的时候触发的。年老代回收也是并发的,但频率比年轻代回收低很多,因为年老代的对象通常都是有用的,不需要频繁回收。

混合回收(Mixed GC)是当年轻代和年老代都需要回收的时候触发的。混合回收会同时回收年轻代和年老代,但主要关注年轻代,年老代只回收一部分。

性能对比

分代模式和单代模式有啥区别呢?第一个区别是吞吐量。分代模式能更高效地回收短命对象,吞吐量通常会更高。根据测试,分代模式的吞吐量比单代模式高 10%-20% 左右。

第二个区别是暂停时间。虽然单代模式的暂停时间已经很短了,但分代模式可以进一步优化,年轻代回收可以更频繁,暂停时间可以更短。根据测试,分代模式的暂停时间比单代模式短 5%-10% 左右。

第三个区别是内存利用率。分代模式能快速回收短命对象,内存利用率通常会更高。根据测试,分代模式的内存利用率比单代模式高 5%-10% 左右。

第四个区别是 CPU 开销。分代模式对年老代扫描更少,CPU 开销可能会更小。但年轻代回收更频繁,CPU 开销可能会稍大一些。总体来说,CPU 开销差不多,或者稍小一些。

实际应用场景

分代模式特别适合哪些场景呢?第一个是大量短命对象的应用,比如 Web 应用、API 服务等,这些应用会创建大量短命对象,分代模式能更高效地回收这些对象。

第二个是对延迟敏感的应用,比如实时系统、游戏服务器等,这些应用需要低延迟,分代模式能提供更短的暂停时间。

第三个是内存受限的应用,比如容器环境、云环境等,这些环境内存有限,分代模式能提高内存利用率,减少内存占用。

第四个是需要高吞吐量的应用,比如批处理系统、数据分析系统等,这些应用需要高吞吐量,分代模式能提供更高的吞吐量。

注意事项

用分代模式的时候,有几个注意事项。第一个是只支持 adaptive 启发式算法,不支持其他算法。如果你想用其他算法,只能用单代模式。

第二个是年轻代大小是自动调整的,不需要手动设置。Shenandoah 会根据应用的行为自动调整年轻代大小,确保回收效率最高。如果你想手动调整,可能需要等后续版本支持。

第三个是分代模式可能不适合所有应用。如果你的应用对象都是长命的,或者对象生命周期没有明显区分,分代模式可能不会有明显优势,甚至可能稍差一些。

第四个是分代模式还在持续优化中,虽然已经是生产特性了,但可能还有改进空间。如果你遇到问题,可以反馈给 OpenJDK 社区。

与其他 GC 的对比

分代 Shenandoah 和其他 GC 有啥区别呢?第一个是暂停时间。Shenandoah 的暂停时间很短,一般在 1-10 毫秒左右,不管堆大小多少。G1 GC 的暂停时间可能更长,特别是堆很大的时候。ZGC 的暂停时间也很短,但可能稍长一些。

第二个是并发性。Shenandoah 的并发性很好,大部分收集工作都是并发执行的,应用不需要停下来等。G1 GC 的并发性也不错,但可能稍差一些。ZGC 的并发性也很好,和 Shenandoah 差不多。

第三个是吞吐量。分代 Shenandoah 的吞吐量通常比单代 Shenandoah 高,但可能比 G1 GC 稍低一些。ZGC 的吞吐量可能和 Shenandoah 差不多,或者稍低一些。

第四个是内存开销。Shenandoah 的内存开销比较大,因为需要额外的元数据。G1 GC 的内存开销也比较大。ZGC 的内存开销可能稍小一些。

调优建议

用分代 Shenandoah 的时候,有几个调优建议。第一个是堆大小要合适,不要太大也不要太小。堆太大会导致回收时间变长,堆太小会导致频繁回收。一般来说,堆大小应该是应用常驻内存的 2-4 倍比较合适。

第二个是年轻代大小让系统自动调整,不要手动设置。Shenandoah 会根据应用的行为自动调整年轻代大小,确保回收效率最高。如果你想手动调整,可能需要等后续版本支持。

第三个是监控 GC 行为,根据实际情况调整。可以用 JFR、jstat 等工具监控 GC 行为,看看回收次数、回收时间、内存使用等情况,根据实际情况调整参数。

第四个是测试不同配置,找出最佳配置。可以测试不同的堆大小、不同的 GC 参数等,找出最适合你应用的配置。

迁移指南

如果你之前用的是单代模式,想迁移到分代模式,有几个建议。第一个是先在测试环境测试,不要直接在生产环境用。分代模式虽然已经是生产特性了,但可能还有问题,先在测试环境测试比较安全。

第二个是逐步迁移,不要一次性迁移所有应用。可以先迁移一个应用,看看效果,如果效果好再迁移其他应用。

第三个是监控性能指标,对比迁移前后的性能。可以监控吞吐量、延迟、内存使用等指标,看看分代模式是否真的提升了性能。

第四个是准备回滚方案,如果分代模式有问题,可以快速回滚到单代模式。回滚很简单,只需要去掉 -XX:ShenandoahGCMode=generational 参数就行。

总结

JEP 521 把分代 Shenandoah 从实验性特性提升为正式的生产特性,这是个重要的改进。分代模式能更高效地回收短命对象,提高吞吐量和内存利用率,减少暂停时间,对需要低延迟和高吞吐量的应用特别有用。

启用分代模式很简单,不需要解锁实验性选项了,直接用 -XX:ShenandoahGCMode=generational 就行。分代模式目前只支持 adaptive 启发式算法,年轻代大小是自动调整的,不需要手动设置。

如果你在用 Shenandoah GC,特别是那些有大量短命对象的应用,建议试试分代模式,它可能会带来明显的性能提升。虽然可能不适合所有应用,但对大多数应用来说,分代模式都是个不错的选择。

本文章最后更新于 2025-11-27