在 Java 应用的性能调优过程中,垃圾收集器(GC)是一个至关重要的环节。尤其是在企业级大内存、高并发场景中,一个选择不当的垃圾收集器可能直接导致系统性能崩溃,甚至生产事故。而在众多 GC 策略中,我几乎在所有的 Java 项目中都优先推荐使用 G1(G1 First)垃圾收集器。本文将深入解析为什么推荐 G1,并结合 JVM 内部结构进行对比、说明其机制与优势。


一、JVM 垃圾收集器的分类

在理解 G1 之前,我们需要了解 Java 垃圾收集器的基本分类。按内存结构划分,GC 策略主要分为两类:

1. 分代收集器(Generational Collectors)

这种方式明确划分 Java 堆为 年轻代(Young Generation)老年代(Old Generation),分别使用不同的垃圾回收算法:

  • 年轻代:使用 Parallel Scavenge(并行收集器)
  • 老年代:使用 Serial Old(串行老年代收集器)

这种搭配是 JDK 1.8 默认的垃圾回收组合

2. 不分代收集器(Partially Generational Collectors)

代表性收集器是 G1(G1 First),在物理上不再明确划分年轻代与老年代,而是通过逻辑上的“分带”方式进行管理,回收机制更加灵活。


二、为什么推荐使用 G1 垃圾收集器?

原因一:解决大内存环境下 STW 卡死问题

在大内存(如超过 32GB)服务器中,Serial Old 收集器因为是 单线程 工作,对老年代的回收效率极低,容易造成 长时间的 STW(Stop The World)。我们曾在生产环境中遇到因 Serial Old 回收老年代而 卡死几个小时 的问题,直接导致服务不可用。

G1 的优势:

  • 使用 并行线程 进行回收;
  • 回收更快,STW 时间更可控;
  • 支持设置 MaxGCPauseMillis,可限制单次 GC 的最长停顿时间(如 200ms),保证系统响应性。

原因二:G1 支持“可控暂停时间”的增量式回收

传统的 PS(Parallel Scavenge)+ Serial Old 是 全量回收,即一旦触发 GC,会尽可能一次性清除所有垃圾,导致长时间的 STW。

G1 的策略是“局部回收”

  • 将堆划分为多个 Region(区域),并按垃圾密度优先清理;
  • 单次 GC 仅选择垃圾最多的区域进行回收;
  • 默认设置 每次 STW 最多 200ms
  • 避免因一次性全量回收而造成长时间卡顿。

例如在互联网应用中,用户等待 200ms 是可接受的,但若 STW 几分钟或几小时,服务将完全崩溃。

原因三:G1 的参数调优更友好

相较于 PS + Serial Old 组合,G1 的调优参数更直观:

  • -XX:+UseG1GC 开启 G1;
  • 可使用 -XX:MaxGCPauseMillis=<N> 来指定最大 GC 停顿时间;
  • 内存分配策略自动化,无需大量调参。

而 PS + Serial Old 的调优往往涉及许多不直观参数,容易调优失败,甚至被称为“玄学调优”。


三、G1 的设计理念与机制

1. Region 分区机制

G1 将堆划分为多个等大小的 Region,每个 Region 的大小固定(如 1MB、2MB、4MB、8MB、16MB、32MB),不可自定义,自动计算分配。

Region 会被逻辑地划分为:

  • Eden 区(E):新对象分配区域
  • Survivor 区(S):幸存对象
  • Old 区(O):长期存活对象

每个区域独立管理,内存碎片少,易于动态调整。

2. 三种垃圾回收类型

(1)Minor GC(年轻代回收)
  • 触发条件:Eden + Survivor 区占比达到堆的 60%
  • 原理:复制算法,将幸存对象移动到 Survivor 区,新对象进 Eden
  • 年龄大于 15 的对象将晋升至老年代
(2)Mixed GC(混合回收)
  • 触发条件:老年代 Region 占比超过堆内存的 45%
  • 回收对象:年轻代 + 部分老年代(垃圾比例高的 Region)

该过程由 4 个阶段组成:

阶段 特点 是否 STW
初始标记 标记 GC Root 可达对象
并发标记 扫描引用链分析对象
最终标记 解决并发中浮动垃圾
筛选回收 标记压缩并回收

其中,标记压缩算法可消除内存碎片。

(3)Full GC(完全回收)
  • 极端情况:老年代持续超过阈值(如 Mixed GC 失败 8 次)
  • 使用 Serial Old 单线程回收
  • 回收时间极长,程序可能进入“死亡状态”

四、G1 的对象晋升机制

G1 在年轻代与老年代之间对象的晋升更灵活:

  • 年龄超过 15,自动晋升至老年代;
  • 如果复制后的对象在 Survivor 区占比超过 50%,最大 age 的对象直接晋升;
  • 避免 Survivor 区过早溢出。

五、如何在 JDK 8 中启用 G1?

虽然 G1 在 JDK 9+ 中是默认垃圾收集器,但在 JDK 8 中需手动配置:

启动参数示例:

-XX:+UseG1GC 
-XX:MaxGCPauseMillis=200 
-Xmx32g 
-Xms32g

说明:

  • -XX:+UseG1GC:启用 G1 收集器
  • -XX:MaxGCPauseMillis:最大单次 GC 停顿时间,默认 200ms,可根据需求调整
  • -Xmx/-Xms:最大/最小堆内存,根据业务规模配置

六、G1 的典型优势总结

特性 说明
分区管理 Region 减少内存碎片,易于管理
多线程回收 利用多核 CPU 并发标记与回收
停顿时间可控 支持设置 MaxGCPauseMillis,实现软实时 GC
增量式局部回收 避免长时间 STW,减少用户可感知卡顿
自动调优 较少参数配置,简单易用
支持大内存(百 GB 以上) 适合现代高内存服务器场景

七、结语

作为 JVM GC 发展史上的一座里程碑,G1 垃圾收集器以其 灵活性、稳定性和高性能 成为目前 Java 项目中最推荐的 GC 策略之一。尤其是在大内存、高并发场景下,PS + Serial Old 组合几乎无法满足业务需求,反而可能引发系统崩溃风险。

在 JDK 8 中,虽然 G1 并非默认,但我们强烈建议手动开启,并使用合理参数进行配置。G1 不仅能带来更短、更稳定的 GC 停顿时间,还大大简化了垃圾收集调优工作,是现代 Java 项目中不可或缺的重要组件。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐