Java 8 (2014年) 是一个里程碑式的版本,其 GC 组合(Parallel GC + CMS/G1)在当时非常经典。但从 Java 9 开始,GC 领域进入了快速迭代和创新的时期,核心目标是:降低停顿时间、提高吞吐量、更好地适应现代大内存和多核硬件,并简化 GC 的使用和调优。

一、核心思想转变:从“选择”到“自适应”

在 Java 8 之前,你需要根据应用场景(吞吐量优先还是低延迟优先)手动选择一个 GC,比如 -XX:+UseParallelGC-XX:+UseConcMarkSweepGC-XX:+UseG1GC
从 Java 9 开始,一个重要的趋势是 “自适应”。JVM 变得更”聪明”,它可以在运行时根据硬件配置和应用行为,动态地调整 GC 的行为,甚至在某些情况下,可以在不同的 GC 算法之间切换,以达到最佳性能。

二、G1

G1((Garbage-First) Garbage Collector (G1GC) - 持续进化,成为默认
G1 在 Java 7 引入,Java 9 中成为 默认的垃圾回收器。它是一个平衡型收集器,旨在兼顾吞吐量和低延迟。
Java 8 之后的关键改进:

  • Java 9:
    • 完全并发的 Class Unloading: 在 Java 8 中,类卸载是在一个安全点暂停中进行的。Java 9 将其改为完全并发执行,进一步减少了 STW 时间。
    • 更优化的 String 去重: G1 可以自动发现堆中重复的 String 对象,并将其指向同一个内部的 char[],节省内存。
  • Java 10:
    • 完全并行的 Full GC: 之前的 Full GC 是单线程的,在堆很大时可能成为性能瓶颈。Java 10 将其改为多线程并行执行,极大地缩短了 Full GC 的停顿时间。
  • Java 12:
    • 可中断的 Mixed GC: 引入了 -XX:+AbortableMixedCollection。当 Mixed GC(混合回收,同时回收新生代和部分老年代)的停顿时间过长时,可以提前中止本次回收,避免对应用造成长时间影响。
  • Java 13/15:
    • 更高效的可撤销 Region 处理: 优化了 G1 在回收过程中的并发阶段,减少了线程间的同步开销,提升了整体效率。
      总结: G1GC 在 Java 8 之后变得越来越成熟和强大,通过一系列优化,巩固了其作为”全能型”默认 GC 的地位。

G1 原理

G1 (Garbage-First) 垃圾回收器采用了一种全新的内存管理策略,其核心思想是将整个 Java 堆划分为多个大小相等的独立区域(Region),每个 Region 都可以扮演 Eden、Survivor 或 Old 区的角色。这种设计使得 G1 能够更加灵活和高效地进行垃圾回收。

核心实现机制

1. Region-based 内存布局

  • 堆被划分为多个大小相等的 Region(默认 1MB-32MB,必须是 2 的幂)
  • 每个 Region 可以是 Eden、Survivor、Old、Humongous(大对象)或空闲状态
  • Humongous Region 用于存储超过单个 Region 50% 大小的对象
  • 这种分区设计使得 G1 可以只选择垃圾最多的 Region 进行回收(Garbage-First 名称的由来)

2. Remembered Set (RSet)

  • 每个 Region 维护一个 RSet,记录哪些其他 Region 的对象引用了本 Region 的对象
  • RSet 通过 Card Table 实现,使用写屏障(Write Barrier)来维护引用关系
  • 当对象引用发生变化时,写屏障会将对应的 Card 标记为 dirty
  • 并发 refinement 线程会定期处理 dirty card,更新 RSet
  • RSet 使得 G1 在回收单个 Region 时无需扫描整个堆

3. Collection Set (CSet)

  • CSet 是本次垃圾回收需要回收的 Region 集合
  • G1 根据每个 Region 的垃圾比例和暂停时间目标来选择 CSet
  • 优先选择垃圾比例最高的 Region(Garbage-First 策略)
  • CSet 的选择是 G1 实现可预测停顿时间的关键

4. 三色标记算法

  • 使用白色(未访问)、灰色(已访问但子对象未处理)、黑色(已完全处理)三色标记
  • 初始标记阶段扫描 GC Roots,将直接引用的对象标记为灰色
  • 并发标记阶段处理灰色对象,将其引用的对象也标记为灰色,自身变为黑色
  • 最终标记阶段处理 SATB 缓冲区中的剩余引用

5. SATB (Snapshot-At-The-Beginning)

  • 为了解决并发标记期间的漏标问题
  • 在标记开始时创建一个逻辑快照,记录当时的对象图
  • 通过写屏障记录并发期间的所有引用变化
  • 确保在标记开始时存活的对象都会被标记为存活

回收过程详解

Young GC 过程:

  1. 根扫描 - 扫描 GC Roots,这个过程需要 STW
  2. 更新 RSet - 处理 dirty card,确保 RSet 数据准确
  3. 处理 RSet - 扫描 CSet 中 Region 的 RSet,找出外部引用
  4. 对象复制 - 将 Eden 和 Survivor 区中的存活对象复制到新的 Survivor 或 Old 区
  5. 引用处理 - 处理软引用、弱引用、虚引用等
  6. 清理 - 释放被回收的 Region,更新元数据

Mixed GC 过程:

  1. 初始标记 - 标记 GC Roots 直接引用的对象,需要 STW
  2. 根区域扫描 - 扫描 Survivor 区指向 Old 区的引用
  3. 并发标记 - 并发标记整个堆中的存活对象
  4. 最终标记 - 处理 SATB 缓冲区,完成标记,需要 STW
  5. 清理 - 计算每个 Region 的存活对象比例,需要 STW
  6. 复制 - 选择垃圾比例高的 Region 进行复制回收

关键技术优化

1. 预测模型

  • G1 使用基于历史数据的预测模型来估算每次 GC 的停顿时间
  • 根据之前 GC 的统计数据预测本次 GC 需要的时间
  • 动态调整 CSet 的大小以满足 MaxGCPauseMillis 目标

2. 并发 refinement

  • 使用多线程并发处理 dirty card,更新 RSet
  • 分为不同级别的 refinement 线程,根据系统负载动态调整
  • 避免在 GC 暂停时集中处理 RSet 更新

3. 字符串去重

  • 在 Java 9+ 中,G1 可以自动发现并去重堆中的 String 对象
  • 通过比较 String 的 hash 值和字符内容来识别重复
  • 将重复的 String 指向同一个内部的 char[],节省内存

4. 可中断的 Mixed GC

  • Java 12+ 引入了可中断的 Mixed GC 功能
  • 当 Mixed GC 的停顿时间超过目标时,可以提前中止
  • 避免长时间的 GC 暂停影响应用响应性

性能调优要点

Region 大小选择:

  • Region 大小对性能有重要影响
  • 过小会导致 RSet 开销增大,过大会降低回收精度
  • 通常建议根据堆大小选择:小堆(1-4GB)用 1MB,中堆(4-16GB)用 4-8MB,大堆(16GB+)用 16-32MB

停顿时间目标:

  • MaxGCPauseMillis 参数设置需要权衡
  • 设置过小会导致频繁 GC,影响吞吐量
  • 建议从 200ms 开始,根据实际监控数据调整

并发线程数:

  • ParallelGCThreads 控制并行阶段线程数
  • ConcGCThreads 控制并发阶段线程数
  • 通常 ConcGCThreads 设置为 ParallelGCThreads 的 1/4 到 1/2

G1GC 常用配置参数详解

G1GC 作为 Java 9+ 的默认垃圾回收器,提供了丰富的配置选项来适应不同的应用场景。以下是生产环境中常用的 G1GC 配置参数:

## 基础启用参数:
# 启用 G1GC(Java 9+ 默认已启用)
-XX:+UseG1GC

## 堆内存配置:
# 设置初始堆大小
-Xms4g
# 设置最大堆大小
-Xmx4g
# 设置新生代初始大小(建议不超过堆的 50%)
-XX:NewSize=2g
# 设置新生代最大大小
-XX:MaxNewSize=2g

## 停顿时间控制:
# 设置最大 GC 停顿时间目标(毫秒),G1GC 会尽力满足这个目标
-XX:MaxGCPauseMillis=200
# 设置 GC 停顿时间间隔目标(毫秒)
-XX:GCPauseIntervalMillis=1000

## Region 相关配置:
# 设置 G1 Region 大小(必须是 2 的幂,范围 1MB-32MB)
-XX:G1HeapRegionSize=16m
# 设置触发 Mixed GC 的老年代占用阈值(百分比)
-XX:InitiatingHeapOccupancyPercent=45
# 设置老年代在 Mixed GC 中的最大回收比例(百分比)
-XX:G1MixedGCCountTarget=8
# 设置 Mixed GC 中老年代 Region 的最小回收比例(百分比)
-XX:G1MixedGCLiveThresholdPercent=85

## 并发线程配置:
# 设置并行 GC 线程数(默认与 CPU 核心数相关)
-XX:ParallelGCThreads=8
# 设置并发 GC 线程数(默认与 ParallelGCThreads 相关)
-XX:ConcGCThreads=4

## 字符串去重优化:
# 启用字符串去重功能(Java 9+ 默认启用)
-XX:+UseStringDeduplication
# 设置字符串去重检查间隔(秒)
-XX:StringDeduplicationAgeThreshold=3

## 可中断 Mixed GC:
# 启用可中断的 Mixed GC(Java 12+)
-XX:+AbortableMixedCollection

## 日志配置:
# 启用 GC 日志
-Xlog:gc*:file=gc.log:time,level,tags
# 或者使用旧版参数(Java 8 风格)
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-Xloggc:gc.log

## 生产环境推荐配置示例:
# 8GB 堆内存,目标停顿时间 200ms
-Xms8g -Xmx8g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
-XX:G1HeapRegionSize=16m
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=4
-Xlog:gc*:file=gc.log:time,level,tags

调优建议:

  1. 从默认配置开始:G1GC 在大多数情况下表现良好,先使用默认配置观察
  2. 关注停顿时间:如果停顿时间过长,逐步调整 -XX:MaxGCPauseMillis
  3. 监控内存使用:通过 GC 日志分析内存分配和回收模式
  4. 避免过度调优:过多的参数调整可能适得其反,每次只调整一个参数

三、ZGC

Z Garbage Collector - 超低延迟的王者,ZGC 是为 超大内存(TB级别)极低停顿时间(目标 < 1ms) 而设计的全新 GC。它从 Java 11 引入,在后续版本中不断完善。
核心特点:

  • 所有阶段都几乎完全并发: 除了短暂的 STW 用于扫描线程栈(Root 扫描),几乎所有工作(标记、转移、重定位)都是并发执行的。
  • 着色指针 & 读屏障: 这是 ZGC 的核心技术。它在对象指针中预留了几个位来标记对象的状态(Finalizable、Remapped、Marked等)。当应用线程需要访问对象时,读屏障会检查指针状态,如果对象正在被移动,它会帮助 GC 完成转移,然后返回新的地址。这使得 GC 可以在不暂停应用线程的情况下移动对象。
  • Region-based 内存布局: 类似 G1,但更灵活,Region 大小可以动态变化(2MB - 512MB)。
  • 不压缩内存: ZGC 在转移对象时,会将其移动到一个新的 Region,但不会对整个堆进行压缩,这简化了设计。
    Java 8 之后的发展:
  • Java 11: 首次引入,但功能有限,不支持类卸载和取消分配。
  • Java 12: 支持 类数据共享,降低了启动时间和内存占用。
  • Java 13: 支持 取消分配,即当 System.gc() 被调用时,ZGC 可以立即释放未使用的内存给操作系统,而不用等待下一次 GC 周期。
  • Java 14: 支持 并发类卸载,补齐了最后一块短板,使其成为一个功能完备的生产级 GC。
  • Java 15: 正式发布,不再是实验性功能。
    适用场景: 对延迟极其敏感的服务,如金融交易、实时竞价、大型在线游戏后台等,以及需要巨大堆内存的应用。

ZGC 的实现

ZGC 常用配置参数详解

ZGC 作为超低延迟的垃圾回收器,配置相对简单,但了解其核心参数对于性能调优至关重要。以下是生产环境中常用的 ZGC 配置参数:

## 基础启用参数:
# 启用 ZGC(Java 15+ 正式支持)
-XX:+UseZGC

# Java 21+ 启用分代式 ZGC(推荐)
-XX:+UseZGC -XX:+ZGenerational

## 堆内存配置:
# 设置初始堆大小
-Xms8g
# 设置最大堆大小(ZGC 支持 TB 级别堆内存)
-Xmx32g

## 并发线程配置:
# 设置并行 GC 线程数(默认与 CPU 核心数相关)
-XX:ParallelGCThreads=16
# 设置并发 GC 线程数(通常设置为 ParallelGCThreads 的 1/4 到 1/2)
-XX:ConcGCThreads=8

## Region 大小配置:
# 设置 ZGC Region 大小(默认自动选择,范围 2MB-512MB)
-XX:ZCollectionInterval=120
# 设置最大 Region 大小
-XX:ZFragmentLimit=25

## 停顿时间控制:
# 设置目标最大停顿时间(毫秒,默认无限制)
-XX:MaxGCPauseMillis=10
# 设置 GC 触发间隔(秒,默认不基于时间触发)
-XX:ZCollectionInterval=5

## 内存分配配置:
# 设置 GC 触发阈值(堆使用百分比,默认 75)
-XX:ZAllocationSpikeTolerance=2
# 设置是否启用大页支持(推荐启用以提高性能)
-XX:+UseLargePages

## 日志配置:
# 启用 GC 日志
-Xlog:gc*:file=zgc.log:time,level,tags
# 详细的 ZGC 日志
-Xlog:gc+heap*=debug,gc+phases*=debug,gc+start*=info:file=zgc.log:time,level,tags

## 生产环境推荐配置示例:
# 32GB 堆内存,超低延迟场景
-Xms32g -Xmx32g
-XX:+UseZGC
-XX:+ZGenerational
-XX:ParallelGCThreads=16
-XX:ConcGCThreads=8
-XX:MaxGCPauseMillis=10
-XX:+UseLargePages
-Xlog:gc*:file=zgc.log:time,level,tags

# 大内存配置(TB 级别)
-Xms1t -Xmx1t
-XX:+UseZGC
-XX:+ZGenerational
-XX:ParallelGCThreads=64
-XX:ConcGCThreads=32
-XX:MaxGCPauseMillis=5
-XX:+UseLargePages
-Xlog:gc*:file=zgc.log:time,level,tags

调优建议:

  1. 从分代式 ZGC 开始:Java 21+ 推荐使用 -XX:+ZGenerational,性能更优
  2. 合理设置堆大小:ZGC 可以处理大内存,但过大的堆会增加单次 GC 的工作量
  3. 关注并发线程数ConcGCThreads 设置过高会与应用线程竞争 CPU,设置过低会延长 GC 时间
  4. 启用大页支持-XX:+UseLargePages 可以显著提高性能,但需要系统配置支持
  5. 监控实际停顿时间:通过日志观察实际停顿时间,调整 MaxGCPauseMillis 目标值
  6. 避免过度调优:ZGC 设计目标就是简单易用,通常默认配置就能提供很好的性能

四、Shenandoah

Shenandoah GC - 另一款超低延迟 GC,Shenandoah 由 Red Hat 主导开发,与 ZGC 目标类似,也是一款追求极低停顿时间的 GC。它从 Java 12 开始被集成。
核心特点:

  • 并发的疏散/转移: 与 ZGC 类似,Shenandoah 也能并发地移动对象。它使用 Brooks Pointers(转发指针)技术。每个对象头都有一个指向自己的指针,当对象需要被移动时,GC 会先复制对象,然后将原对象的转发指针指向新地址。应用线程通过这个转发指针来访问对象,从而实现并发转移。
  • 与 ZGC 的主要区别:
    • 技术实现不同: ZGC 用着色指针,Shenandoah 用转发指针。着色指针对 CPU 架构有一定要求(需要足够的空闲位),而转发指针更通用。
    • 内存占用: Shenandoah 的转发指针会带来一些额外的内存开销。
    • 社区与生态: ZGC 由 Oracle 主导,与 OpenJDK 关系更紧密;Shenandoah 由 Red Hat 主导。
      Java 8 之后的发展:
  • Java 12: 作为实验性功能引入。
  • Java 15: 正式发布
    适用场景: 与 ZGC 类似,适用于对延迟要求苛刻的场景。选择 ZGC 还是 Shenandoah,有时取决于具体的 JVM 发行版(如 Oracle JDK vs. OpenJDK)和硬件平台。

五、被淘汰的 GC

CMS (Concurrent Mark Sweep)

  • Java 8: 仍然可用,但已被标记为 Deprecated
  • Java 9: 仍然可用,但使用时会收到警告,提示未来版本中会被移除。
  • Java 14: 被彻底移除
    淘汰原因:
  1. 碎片化问题: CMS 使用标记-清除算法,会产生大量内存碎片,可能导致提前触发 Full GC。
  2. 并发模式失败: 在并发标记期间,如果应用线程分配速度过快,导致老年代空间不足,会触发一次耗时很长的“并发模式失败”的 Full GC。
  3. 代码维护复杂: 其内部实现非常复杂,难以维护和扩展。
    G1GC 的出现,在提供类似并发回收能力的同时,通过 Region 化设计解决了碎片化问题,成为 CMS 的完美替代品。

六、新的 GC 特性与实验性项目

1. Epsilon GC - “No-Op” GC

  • 引入版本: Java 11 (实验性)
  • 作用: 它是一个“什么都不做”的 GC。它只负责内存分配,从不进行回收。当堆内存耗尽时,JVM 就会崩溃。
  • 用途:
    • 性能测试: 用于测试应用本身的最大吞吐量,排除 GC 的干扰。
    • 极短生命周期的应用: 比如一些函数计算任务,运行时间很短,在内存耗尽前就结束了。
    • VM 接口测试: 用于测试 JVM 的其他组件,而不需要 GC 参与。

2. Generational ZGC (分代式 ZGC)

  • 这是当前 GC 领域最激动人心的进展!
  • 背景: ZGC 和 Shenandoah 都是非分代 GC。而传统的分代 GC(如 G1)基于一个经验:绝大多数对象都是“朝生夕死”的。通过只回收新生代,可以用很低的成本回收大量内存,效率极高。
  • 问题: 非分代 GC 每次都要扫描整个堆,对于堆中存在大量“垃圾”的应用来说,效率不高。
  • 解决方案: 为 ZGC 引入分代设计,使其同时拥有 分代 GC 的高效率ZGC 的超低停顿
  • 进展:
    • Java 21: 分代式 ZGC 作为预览功能发布 (-XX:+UseZGC -XX:+ZGenerational)。
    • Java 23: 分代式 ZGC 转为正式功能
  • 意义: 这可能是未来 GC 的主流方向,它有望在几乎所有场景下都提供比 G1 更好的性能(更低的停顿和相当的吞吐量)。

七、总结与选择建议

GC 名称目标引入/默认版本停顿时间吞吐量适用场景
CMS低延迟 (已过时)Java 9 废弃, 14 移除--不要在新项目中使用
Parallel GC高吞吐量Java 8 默认较长最高后台计算、批处理任务,对停顿不敏感
G1 GC平衡延迟与吞吐Java 9+ 默认中低 (可预测)较高通用场景,大多数服务端应用的首选
ZGC超低延迟Java 11 引入, 15 正式极低 (< 1ms)中高大内存、对延迟极度敏感的应用
Shenandoah超低延迟Java 12 引入, 15 正式极低中高与 ZGC 类似,Red Hat 生态下的首选

给开发者的建议:

  1. 从默认开始: 对于 Java 9+ 的应用,直接使用默认的 G1GC。它在绝大多数情况下都是一个很好的起点。
  2. 评估你的需求:
    • 如果你的应用是纯粹的 CPU 密集型批处理,且允许较长的 GC 停顿,可以尝试 Parallel GC (-XX:+UseParallelGC)。
    • 如果你的应用对延迟非常敏感(例如,99.9% 的请求必须在 10ms 内响应),并且你的堆内存较大(> 8GB),强烈建议尝试 ZGC (-XX:+UseZGC)。在 Java 21+ 中,直接使用分代式 ZGC。
  3. 拥抱新特性: 如果你的 JDK 版本较新,不要害怕使用 ZGC 等新一代 GC。它们的设计就是为了简化调优,通常只需要开启开关,无需过多参数调整就能获得优异性能。