Java gc
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(混合回收,同时回收新生代和部分老年代)的停顿时间过长时,可以提前中止本次回收,避免对应用造成长时间影响。
- 可中断的 Mixed GC: 引入了
- Java 13/15:
- 更高效的可撤销 Region 处理: 优化了 G1 在回收过程中的并发阶段,减少了线程间的同步开销,提升了整体效率。
总结: G1GC 在 Java 8 之后变得越来越成熟和强大,通过一系列优化,巩固了其作为”全能型”默认 GC 的地位。
- 更高效的可撤销 Region 处理: 优化了 G1 在回收过程中的并发阶段,减少了线程间的同步开销,提升了整体效率。
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 过程:
- 根扫描 - 扫描 GC Roots,这个过程需要 STW
- 更新 RSet - 处理 dirty card,确保 RSet 数据准确
- 处理 RSet - 扫描 CSet 中 Region 的 RSet,找出外部引用
- 对象复制 - 将 Eden 和 Survivor 区中的存活对象复制到新的 Survivor 或 Old 区
- 引用处理 - 处理软引用、弱引用、虚引用等
- 清理 - 释放被回收的 Region,更新元数据
Mixed GC 过程:
- 初始标记 - 标记 GC Roots 直接引用的对象,需要 STW
- 根区域扫描 - 扫描 Survivor 区指向 Old 区的引用
- 并发标记 - 并发标记整个堆中的存活对象
- 最终标记 - 处理 SATB 缓冲区,完成标记,需要 STW
- 清理 - 计算每个 Region 的存活对象比例,需要 STW
- 复制 - 选择垃圾比例高的 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调优建议:
- 从默认配置开始:G1GC 在大多数情况下表现良好,先使用默认配置观察
- 关注停顿时间:如果停顿时间过长,逐步调整
-XX:MaxGCPauseMillis - 监控内存使用:通过 GC 日志分析内存分配和回收模式
- 避免过度调优:过多的参数调整可能适得其反,每次只调整一个参数
三、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调优建议:
- 从分代式 ZGC 开始:Java 21+ 推荐使用
-XX:+ZGenerational,性能更优 - 合理设置堆大小:ZGC 可以处理大内存,但过大的堆会增加单次 GC 的工作量
- 关注并发线程数:
ConcGCThreads设置过高会与应用线程竞争 CPU,设置过低会延长 GC 时间 - 启用大页支持:
-XX:+UseLargePages可以显著提高性能,但需要系统配置支持 - 监控实际停顿时间:通过日志观察实际停顿时间,调整
MaxGCPauseMillis目标值 - 避免过度调优: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: 被彻底移除。
淘汰原因:
- 碎片化问题: CMS 使用标记-清除算法,会产生大量内存碎片,可能导致提前触发 Full GC。
- 并发模式失败: 在并发标记期间,如果应用线程分配速度过快,导致老年代空间不足,会触发一次耗时很长的“并发模式失败”的 Full GC。
- 代码维护复杂: 其内部实现非常复杂,难以维护和扩展。
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 转为正式功能。
- Java 21: 分代式 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 生态下的首选 |
给开发者的建议:
- 从默认开始: 对于 Java 9+ 的应用,直接使用默认的 G1GC。它在绝大多数情况下都是一个很好的起点。
- 评估你的需求:
- 如果你的应用是纯粹的 CPU 密集型批处理,且允许较长的 GC 停顿,可以尝试
Parallel GC(-XX:+UseParallelGC)。 - 如果你的应用对延迟非常敏感(例如,99.9% 的请求必须在 10ms 内响应),并且你的堆内存较大(> 8GB),强烈建议尝试 ZGC (
-XX:+UseZGC)。在 Java 21+ 中,直接使用分代式 ZGC。
- 如果你的应用是纯粹的 CPU 密集型批处理,且允许较长的 GC 停顿,可以尝试
- 拥抱新特性: 如果你的 JDK 版本较新,不要害怕使用 ZGC 等新一代 GC。它们的设计就是为了简化调优,通常只需要开启开关,无需过多参数调整就能获得优异性能。

