JAVA 大厂常用 GC 调优方案详解:G1、ZGC、Shenandoah 对比分析
大家好,今天我们来深入探讨一下 Java 大厂常用的 GC 调优方案,重点对比分析 G1、ZGC 和 Shenandoah 这三种 GC 算法。垃圾回收(GC)是 Java 虚拟机(JVM)的重要组成部分,它负责自动管理内存,回收不再使用的对象,防止内存泄漏。一个优秀的 GC 策略能够显著提升应用程序的性能和稳定性。
1. GC 的基本概念回顾
在深入了解具体的 GC 算法之前,我们先简单回顾一些 GC 的基本概念:
- 新生代(Young Generation): 对象刚创建时通常位于新生代,分为 Eden 区和两个 Survivor 区(S0 和 S1)。
 - 老年代(Old Generation): 经过多次 Minor GC 仍然存活的对象会被移动到老年代。
 - 永久代/元空间(Permanent Generation/Metaspace): 用于存储类信息、常量池等数据。在 JDK 8 之后,永久代被元空间取代,元空间使用本地内存。
 - Minor GC(Young GC): 对新生代进行垃圾回收。
 - Major GC(Old GC): 对老年代进行垃圾回收。
 - Full GC: 对整个堆(包括新生代、老年代和元空间)进行垃圾回收。Full GC 通常会造成较长的停顿时间。
 - Stop-The-World (STW): 在 GC 过程中,JVM 会暂停所有应用程序线程,以便安全地进行垃圾回收。STW 的时间越短,应用程序的响应性越好。
 - 吞吐量(Throughput): 应用程序运行的时间占总时间的比例。高吞吐量意味着应用程序能够处理更多的任务。
 - 延迟(Latency): 应用程序的响应时间。低延迟意味着应用程序能够更快地响应用户请求。
 
2. G1 (Garbage-First) 垃圾收集器
G1 是 JDK 7 Update 4 中引入的垃圾收集器,并在 JDK 9 中成为默认的垃圾收集器。它的设计目标是替代 CMS 收集器,并提供更好的预测性和可控性。
2.1 G1 的特点:
- 基于 Region 的堆内存布局: G1 将堆内存划分为多个大小相等的 Region,每个 Region 可以是 Eden 区、Survivor 区或老年代。
 - Garbage-First: G1 优先回收垃圾最多的 Region,以最大化每次 GC 的回收效率。
 - 可预测的停顿时间: G1 允许用户指定期望的停顿时间,G1 会尽力满足这个目标。
 - 并发标记: G1 使用并发标记算法,在应用程序运行的同时进行垃圾标记,减少 STW 时间。
 - 空间整合: G1 在 GC 过程中会对空闲 Region 进行整合,避免内存碎片。
 
2.2 G1 的工作流程:
- 初始标记(Initial Mark): 标记 GCRoots 可达的对象。需要 STW。
 - 并发标记(Concurrent Marking): 从 GCRoots 开始,并发遍历堆中的对象,标记所有可达的对象。
 - 最终标记(Remark): 修正并发标记期间应用程序线程对对象引用关系产生的变动。需要 STW。
 - 筛选回收(Cleanup): 对各个 Region 的回收价值和成本进行排序,根据用户期望的停顿时间制定回收计划。然后将垃圾最多的 Region 中的垃圾对象回收。部分过程是并发的。
 - 复制/转移(Evacuation): 将存活的对象复制到新的 Region 中,并更新引用。需要 STW。
 
2.3 G1 的常用 JVM 参数:
| 参数 | 说明 | 默认值 | 
|---|---|---|
-XX:+UseG1GC | 
启用 G1 垃圾收集器 | |
-XX:MaxGCPauseMillis=n | 
设置期望的停顿时间,单位为毫秒。G1 会尽力满足这个目标,但这并不保证一定能够达到。 | 200ms | 
-XX:G1HeapRegionSize=size | 
设置每个 Region 的大小。通常设置为 1MB 到 32MB 之间,G1 会自动调整 Region 的数量。 | 自动计算 | 
-XX:G1NewSizePercent=percent | 
设置新生代占堆内存的最小百分比。 | 5% | 
-XX:G1MaxNewSizePercent=percent | 
设置新生代占堆内存的最大百分比。 | 60% | 
-XX:InitiatingHeapOccupancyPercent=percent | 
设置触发并发 GC 的堆内存使用百分比。当堆内存使用超过这个百分比时,G1 会启动并发 GC。 | 45% | 
-XX:ConcGCThreads=n | 
设置并发 GC 的线程数。 | 根据 CPU 核心数自动计算 | 
-XX:G1ReservePercent=percent | 
设置预留的内存百分比,用于在 GC 过程中分配新的对象。 | 10% | 
-XX:+PrintGCDetails | 
打印详细的 GC 日志。 | |
-XX:+PrintGCTimeStamps | 
打印 GC 时间戳。 | |
-Xmx<size> | 
设置堆内存的最大大小。 | |
-Xms<size> | 
设置堆内存的初始大小。 | 
2.4 G1 的代码示例:
public class G1Example {
    public static void main(String[] args) throws InterruptedException {
        // 模拟对象分配
        for (int i = 0; i < 1000000; i++) {
            byte[] data = new byte[1024]; // 分配 1KB 的内存
            // 让部分对象存活更长时间
            if (i % 100 == 0) {
                Thread.sleep(1);
            }
        }
        System.out.println("对象分配完成");
    }
}
// 运行参数示例:
// java -Xmx2g -Xms2g -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps G1Example
这个示例模拟了对象的分配,并设置了 G1 的一些常用参数,如最大堆内存、初始堆内存、使用 G1 GC、最大 GC 停顿时间等。可以通过观察 GC 日志来分析 G1 的行为。
2.5 G1 的适用场景:
- 需要低延迟的应用,例如在线交易系统、实时数据处理系统。
 - 大堆内存的应用,例如大数据分析、机器学习。
 - 不希望频繁进行 Full GC 的应用。
 
2.6 G1 的调优建议:
- 合理设置 
-XX:MaxGCPauseMillis: 根据应用程序的实际需求设置期望的停顿时间。如果设置得太小,可能会导致 G1 频繁进行 GC,影响吞吐量。 - 监控 GC 日志: 通过 GC 日志分析 G1 的行为,例如 GC 的频率、停顿时间、堆内存的使用情况等。
 - 调整 Region 大小:  
-XX:G1HeapRegionSize可以调整 Region 的大小。较小的 Region 可以提高空间利用率,但可能会增加 GC 的频率。较大的 Region 可以减少 GC 的频率,但可能会浪费空间。 - 调整并发 GC 线程数:  
-XX:ConcGCThreads可以调整并发 GC 的线程数。过多的线程可能会占用过多的 CPU 资源,影响应用程序的性能。过少的线程可能会导致 GC 的速度过慢。 - 避免过多的 Full GC: 如果 G1 频繁进行 Full GC,需要检查代码中是否存在内存泄漏或其他问题。
 
3. ZGC (Z Garbage Collector) 垃圾收集器
ZGC 是 JDK 11 中引入的低延迟垃圾收集器。它的设计目标是实现亚毫秒级的 GC 停顿时间,即使在 TB 级别的堆内存下也能保持良好的性能。
3.1 ZGC 的特点:
- 基于 Region 的堆内存布局: ZGC 也将堆内存划分为多个 Region,但 Region 的大小可以动态变化。
 - 着色指针(Colored Pointers): ZGC 使用着色指针技术,将一些 GC 信息存储在指针中,从而避免在 GC 过程中遍历整个堆。
 - 读屏障(Read Barriers): ZGC 使用读屏障技术,在应用程序读取对象引用时进行一些额外的操作,例如检查对象是否被移动。
 - 并发: ZGC 几乎所有的 GC 工作都是并发执行的,包括标记、转移和重定位。
 - 亚毫秒级停顿: ZGC 的停顿时间非常短,通常在几毫秒以内。
 
3.2 ZGC 的工作流程:
- 并发标记(Concurrent Mark): 从 GCRoots 开始,并发遍历堆中的对象,标记所有可达的对象。
 - 并发预备重定位(Concurrent Prepare Relocate): 统计 Region 中的存活对象,并为每个 Region 选择合适的转移目标。
 - 并发重定位(Concurrent Relocate): 将存活的对象复制到新的 Region 中,并更新引用。
 - 并发重映射(Concurrent Remap): 修正所有指向已转移对象的指针。
 
3.3 ZGC 的常用 JVM 参数:
| 参数 | 说明 | 默认值 | 
|---|---|---|
-XX:+UseZGC | 
启用 ZGC 垃圾收集器 | |
-Xmx<size> | 
设置堆内存的最大大小。ZGC 对堆内存的大小没有特别的限制,但建议设置足够大的堆内存以避免频繁 GC。 | |
-Xms<size> | 
设置堆内存的初始大小。通常设置为与最大堆内存相同,以避免堆内存的动态扩展。 | |
-XX:ConcGCThreads=n | 
设置并发 GC 的线程数。 | 根据 CPU 核心数自动计算 | 
-XX:+PrintGCDetails | 
打印详细的 GC 日志。 | |
-XX:+PrintGCTimeStamps | 
打印 GC 时间戳。 | |
-XX:+ZUncommit | 
启用内存释放功能。ZGC 可以将不再使用的内存释放回操作系统。 | |
-XX:ZUncommitDelay=seconds | 
设置内存释放的延迟时间。 | 300 秒 | 
3.4 ZGC 的代码示例:
public class ZGCExample {
    public static void main(String[] args) throws InterruptedException {
        // 模拟对象分配
        for (int i = 0; i < 10000000; i++) {
            byte[] data = new byte[1024]; // 分配 1KB 的内存
            // 让部分对象存活更长时间
            if (i % 1000 == 0) {
                Thread.sleep(1);
            }
        }
        System.out.println("对象分配完成");
    }
}
// 运行参数示例:
// java -Xmx4g -Xms4g -XX:+UseZGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps ZGCExample
这个示例与 G1 的示例类似,但使用了 ZGC 垃圾收集器。可以通过观察 GC 日志来分析 ZGC 的行为。
3.5 ZGC 的适用场景:
- 需要极低延迟的应用,例如金融交易系统、实时游戏。
 - 大堆内存的应用,例如 TB 级别的内存数据库。
 - 对吞吐量要求不高的应用。
 
3.6 ZGC 的调优建议:
- 设置足够大的堆内存: ZGC 的性能与堆内存的大小密切相关。建议设置足够大的堆内存,以避免频繁 GC。
 - 监控 GC 日志: 通过 GC 日志分析 ZGC 的行为,例如 GC 的频率、停顿时间、堆内存的使用情况等。
 - 调整并发 GC 线程数:  
-XX:ConcGCThreads可以调整并发 GC 的线程数。 - 启用内存释放功能:  
-XX:+ZUncommit可以启用内存释放功能,将不再使用的内存释放回操作系统。 
4. Shenandoah 垃圾收集器
Shenandoah 是 Red Hat 开发的一款并发垃圾收集器,于 JDK 12 中正式发布。它的目标与 ZGC 类似,都是实现亚毫秒级的 GC 停顿时间。
4.1 Shenandoah 的特点:
- 基于 Region 的堆内存布局: Shenandoah 也将堆内存划分为多个 Region。
 - 着色指针(Colored Pointers): 与 ZGC 类似,Shenandoah 也使用着色指针技术。
 - 转发指针(Forwarding Pointers): Shenandoah 使用转发指针技术,在对象被移动时,将旧的对象指向新的对象。
 - 并发: Shenandoah 几乎所有的 GC 工作都是并发执行的。
 - 亚毫秒级停顿: Shenandoah 的停顿时间非常短,通常在几毫秒以内。
 
4.2 Shenandoah 的工作流程:
- 初始标记(Initial Mark): 标记 GCRoots 可达的对象。需要 STW。
 - 并发标记(Concurrent Mark): 从 GCRoots 开始,并发遍历堆中的对象,标记所有可达的对象。
 - 最终标记(Final Mark): 修正并发标记期间应用程序线程对对象引用关系产生的变动。需要 STW。
 - 并发清理(Concurrent Cleanup): 清理不再使用的 Region。
 - 并发转移(Concurrent Evacuation): 将存活的对象复制到新的 Region 中,并更新引用。
 - 初始引用更新(Initial Reference Update): 准备更新所有指向已转移对象的指针。需要 STW。
 - 并发引用更新(Concurrent Reference Update): 并发更新所有指向已转移对象的指针。
 - 最终引用更新(Final Reference Update): 完成引用更新。需要 STW。
 
4.3 Shenandoah 的常用 JVM 参数:
| 参数 | 说明 | 默认值 | 
|---|---|---|
-XX:+UseShenandoahGC | 
启用 Shenandoah 垃圾收集器 | |
-Xmx<size> | 
设置堆内存的最大大小。 | |
-Xms<size> | 
设置堆内存的初始大小。 | |
-XX:ConcGCThreads=n | 
设置并发 GC 的线程数。 | 根据 CPU 核心数自动计算 | 
-XX:+PrintGCDetails | 
打印详细的 GC 日志。 | |
-XX:+PrintGCTimeStamps | 
打印 GC 时间戳。 | |
-XX:ShenandoahGCHeuristics=<value> | 
设置 Shenandoah 的启发式算法。 | adaptive | 
4.4 Shenandoah 的代码示例:
public class ShenandoahExample {
    public static void main(String[] args) throws InterruptedException {
        // 模拟对象分配
        for (int i = 0; i < 10000000; i++) {
            byte[] data = new byte[1024]; // 分配 1KB 的内存
            // 让部分对象存活更长时间
            if (i % 1000 == 0) {
                Thread.sleep(1);
            }
        }
        System.out.println("对象分配完成");
    }
}
// 运行参数示例:
// java -Xmx4g -Xms4g -XX:+UseShenandoahGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps ShenandoahExample
这个示例与 G1 和 ZGC 的示例类似,但使用了 Shenandoah 垃圾收集器。可以通过观察 GC 日志来分析 Shenandoah 的行为。
4.5 Shenandoah 的适用场景:
- 需要极低延迟的应用,例如金融交易系统、实时游戏。
 - 大堆内存的应用。
 - 对 CPU 资源充足的应用。
 
4.6 Shenandoah 的调优建议:
- 监控 GC 日志: 通过 GC 日志分析 Shenandoah 的行为,例如 GC 的频率、停顿时间、堆内存的使用情况等。
 - 调整并发 GC 线程数:  
-XX:ConcGCThreads可以调整并发 GC 的线程数。 - 选择合适的启发式算法:  
-XX:ShenandoahGCHeuristics可以设置 Shenandoah 的启发式算法。 
5. G1、ZGC 和 Shenandoah 的对比
| 特性 | G1 | ZGC | Shenandoah | 
|---|---|---|---|
| 停顿时间 | 可预测的停顿时间,通常在几百毫秒以内。 | 亚毫秒级停顿。 | 亚毫秒级停顿。 | 
| 吞吐量 | 相对较高。 | 相对较低。 | 相对较低。 | 
| 堆内存大小 | 适用于中大型堆内存(几 GB 到几十 GB)。 | 适用于大型堆内存(TB 级别)。 | 适用于大型堆内存。 | 
| 并发性 | 部分并发。 | 几乎完全并发。 | 几乎完全并发。 | 
| 内存碎片 | 可以进行空间整合,避免内存碎片。 | 具有良好的内存碎片管理能力。 | 具有良好的内存碎片管理能力。 | 
| 适用场景 | 需要低延迟和高吞吐量的应用,例如在线交易系统、实时数据处理系统。 | 需要极低延迟的应用,例如金融交易系统、实时游戏。 | 需要极低延迟的应用。 | 
| 复杂度 | 相对简单。 | 复杂。 | 复杂。 | 
| 额外开销 | 需要一定的 CPU 资源进行垃圾回收。 | 需要较多的 CPU 资源进行垃圾回收。 | 需要较多的 CPU 资源进行垃圾回收。 | 
| 发展程度 | 较为成熟,应用广泛。 | 相对较新,仍在发展中。 | 相对较新,仍在发展中。 | 
6. 如何选择合适的 GC 算法
选择合适的 GC 算法需要根据应用程序的实际需求进行权衡。
- 如果应用程序对延迟非常敏感,且堆内存较大,可以考虑使用 ZGC 或 Shenandoah。
 - 如果应用程序对吞吐量要求较高,且对延迟的要求不是特别严格,可以考虑使用 G1。
 - 如果应用程序的堆内存较小,且对性能的要求不高,可以使用 Serial 或 Parallel 收集器。
 
在选择 GC 算法时,还需要考虑以下因素:
- 硬件资源: ZGC 和 Shenandoah 需要较多的 CPU 资源。
 - 应用程序的负载模式: 不同的负载模式对 GC 的性能有不同的影响。
 - JVM 的版本: 不同的 JVM 版本对 GC 算法的支持程度不同。
 
建议在生产环境中进行充分的测试,以确定最适合应用程序的 GC 算法。
7. 总结:根据需求选择,持续监控调优
选择合适的 GC 算法需要根据应用场景和性能指标进行权衡。G1、ZGC 和 Shenandoah 各有优缺点,需要根据实际情况进行选择。在选择后,还需要通过监控 GC 日志和性能指标,持续进行调优,以达到最佳的性能。