概述
纵观 Java 的发展历程,一个完整意义上的 Java 虚拟机(以主流的 HotSpot VM 为例),其垃圾回收器的名单有很多,包括了负责不同分区的老牌回收器,以及一些有特定用途的“特种”回收器。
下面这张图可以帮你从整体上理解它们的分类和配合关系:
🔄 经典分代收集器
这是 JVM 历史上最经典的一类回收器。它们严格遵循“分代”理论,将堆内存物理地划分为新生代和老年代,并在不同区域使用不同的回收算法。
| 回收器 | 所属年代 | 算法 | 线程模型 | 特点与地位 | 状态 |
|---|---|---|---|---|---|
| Serial | 新生代 | 复制算法 | 单线程 | 最基础的回收器。进行垃圾回收时会触发“Stop-The-World”(STW,即暂停所有应用线程)。 | 历史 |
| Serial Old | 老年代 | 标记-整理算法 | 单线程 | Serial 的老年代版本。常作为 CMS 的后备方案。 | 历史 |
| ParNew | 新生代 | 复制算法 | 多线程 | Serial 的多线程版本。能与 CMS 配合,是其老搭档。 | 历史 |
| Parallel Scavenge | 新生代 | 复制算法 | 多线程 | JDK 8 及之前版本的默认收集器。侧重点是吞吐量优先。 | 成熟 |
| Parallel Old | 老年代 | 标记-整理算法 | 多线程 | Parallel Scavenge 的老年代版本,同样侧重吞吐量。是 JDK 8 默认的 GC 组合。 | 成熟 |
| CMS | 老年代 | 标记-清除算法 | 多线程 | 首款真正意义上的并发收集器,为追求低延迟而生。已在 JDK 14 中被正式移除。 | 淘汰 |
🗺️ 分区与超低延迟收集器
随着硬件发展和应用对响应时间的要求越来越高,GC 的设计开始从关注“吞吐量”转向关注“低延迟”。这一代的回收器打破了物理分代的限制,采用了更灵活的内存布局和算法。
| 回收器 | 核心算法 | 特点与地位 | 默认/推荐JDK |
|---|---|---|---|
| G1 | Region + 复制算法 | 平衡吞吐量和延迟,提供可预测的停顿时间模型。自 JDK 9 起成为默认垃圾回收器。 | JDK 9+ 默认 |
| ZGC | Region + 染色指针 + 读屏障 | 追求极致的低延迟(亚毫秒级停顿),暂停时间不随堆大小增长。JDK 15 起生产可用。从 JDK 21 开始,引入了分代 ZGC 以进一步提升性能。 | JDK 21+ 推荐 |
| Shenandoah | Region + 转发指针 + 读屏障 | 与 ZGC 目标相似,同样追求超低延迟,主要由 OpenJDK 社区维护。JDK 15 起生产可用。 | JDK 15+ 可用 |
🛠️ 特殊用途收集器
除了通用回收器,Java 还提供了一些用于特定场景的“特种工具”。
- Epsilon GC:一个“无操作”(No-Op)的回收器。它只分配内存,从不回收,当堆内存耗尽时,JVM 会直接退出。主要用于性能测试、内存压力测试等,以排除 GC 本身带来的性能干扰。
- C4 (Continuously Concurrent Compacting Collector):这是高性能商业虚拟机 Azul Zing 中的 GC。它利用“读屏障”等技术,实现了无停顿的并发压缩,即使是处理 TB 级的堆内存也能保持毫秒级的暂停。
- Shenandoah:OpenJDK 的“超低延迟”GC。与 ZGC 类似,也使用转发指针和读屏障技术,通过“Brooks Pointer”实现并发对象移动。它是 OpenJDK 独有的项目,不在 OracleJDK 中提供。
要获取你当前 Java 环境使用的垃圾回收器组合,可以在命令行中尝试运行:
java -XX:+PrintCommandLineFlags -version
常用垃圾回收器
Java常用垃圾回收器有Parallel Scavenge / Parallel Old ,CMS,G1,ZGC。要理解这几个垃圾回收器,关键在于把握 吞吐量、延迟(STW暂停时间) 和 内存大小 这几个核心维度的权衡。
下面我们逐一解析。
1. Parallel Scavenge / Parallel Old
这是JDK 8及之前版本的默认GC组合,属于并行、分代、吞吐量优先的回收器。
-
工作原理:
- Parallel Scavenge(年轻代):采用复制算法。GC触发时,所有工作线程同时暂停(Stop-The-World,STW),并行地复制存活对象到Survivor或老年代。
- Parallel Old(老年代):采用标记-整理算法。同样STW,并行完成标记和整理,消除内存碎片。
- 核心特点:它可以主动调节堆大小和分区比例,来达到用户设定的吞吐量目标(例如
-XX:GCTimeRatio=99表示GC时间占比不超过1%)。
-
优点:
- 吞吐量极高:因为GC时所有CPU核心都用来做清理,没有并发阶段的额外开销。
- CPU利用率高:非常适合计算密集型任务。
- 实现简单,管理内存开销小。
-
缺点:
- 延迟高:GC暂停时间与堆内存大小成正比。堆越大,暂停时间越长(可能达到数秒甚至十秒级)。
- 不可控:无法将最大暂停时间控制在几十毫秒内。
-
适用场景:
- 批处理任务、大数据计算(如Hadoop离线任务、Spark SQL)。
- 后台作业(编译、压测、科学计算)。
- 对响应时间不敏感,但对处理数据总量有要求的场景。
- 堆内存不大(< 4GB)且CPU核数多的环境。
2. CMS
全称 Concurrent Mark Sweep,是JDK 8时代追求低延迟的首选,但已在JDK 9中标记为废弃,JDK 14中彻底移除。
-
工作原理:核心是分代 + 标记-清除 + 并发回收。主要分为4个阶段:
- 初始标记(STW):快速标记GC Roots直接引用的对象(暂停很短)。
- 并发标记:遍历整个对象图,标记所有存活对象(与应用线程并发执行,耗时较长但无暂停)。
- 重新标记(STW):修正并发标记期间因用户程序运行而变动的标记(暂停时间比初始标记稍长,但仍可控)。
- 并发清除:清除未被标记的对象(并发执行)。
-
优点:
- 低延迟:最耗时的两个阶段(并发标记、并发清除)都不暂停应用,整体STW时间远低于Parallel。
- 响应快:适合交互式应用。
-
缺点:
- 产生内存碎片:标记-清除算法会导致大量不连续内存,最终可能因无法分配大对象而触发Full GC(此时退化为Serial Old,暂停时间极长)。
- 浮动垃圾:并发清理时用户线程仍在运行,新产生的垃圾本次无法处理,需要预留空间。
- CPU敏感:并发阶段会抢占应用线程的CPU资源,导致总吞吐量下降。
- 可能发生“并发模式失败”:如果老年代填满速度超过CMS回收速度,会降级为Serial Old进行Full GC,导致长时间停顿。
- 对大型堆(>32GB)表现不佳。
-
适用场景:
- Web服务器、微服务(低延迟要求高,但堆内存不超过32GB)。
- 中小型内存(4GB - 32GB)的在线应用。
- 需要比Parallel更低的暂停时间,但又承受不了G1的额外开销(注意:现在G1通常是更好的选择)。
3. G1
全称 Garbage First,从JDK 9开始成为默认GC,目标是在可控的暂停时间内实现高吞吐。
-
工作原理:
- 分Region:不再区分物理上的年轻代和老年代,而是将整个堆划分为约2048个大小相等的Region。
- 停顿预测模型:用户可以指定期望的暂停时间(如
-XX:MaxGCPauseMillis=200),G1会根据历史数据动态选择一批Region来回收(这就是“Garbage First”名字的由来——优先回收垃圾最多的Region)。 - 回收过程:
- 年轻代回收:将Eden区存活对象复制到Survivor或直接晋升到老年代Region。全程STW但并行执行。
- 并发标记:类似CMS的并发标记,找出老年代Region中的垃圾。
- 混合回收:同时回收年轻代和部分高价值的老年代Region,使用复制算法消除碎片。
- 使用SATB(Snapshot-At-The-Beginning)算法处理并发标记时的对象引用变化。
-
优点:
- 可预测的暂停时间:通过控制每次回收的Region数量,将暂停时间控制在用户期望范围内(通常几十到两百毫秒)。
- 自动管理:无需手动设定年轻代/老年代大小。
- 内存整合:本质上是复制算法,无内存碎片。
- 大堆支持良好:设计目标之一是处理数十GB的堆。
-
缺点:
- CPU和内存开销略高:需要维护Remembered Set(RSet)来记录跨Region引用,占额外内存(大约堆的5%-10%)。
- 吞吐量略低于Parallel:因为有一些维护和并发的开销。
- Full GC可能发生:当对象复制速度跟不上分配速度时,会退化为单线程Serial Full GC(JDK 10以后可并行Full GC)。
-
适用场景:
- 中等及以上堆内存(6GB - 64GB,甚至更大)。
- 对暂停时间有明确要求(例如要求GC暂停<200ms)。
- 替代CMS:需要低延迟但不想处理CMS的碎片和并发模式失败问题。
- 大多数通用服务端应用(Spring Boot微服务、交易系统等)。
4. ZGC
全称 Z Garbage Collector,从JDK 11引入,目标是极其低延迟(暂停时间不超过1ms,甚至<0.5ms),且堆容量可到16TB。
-
工作原理:
- 染色指针:将指针的高位(额外64位中的几位)用于存储对象状态信息(如是否可访问、是否在重定位中),因此无需在对象头中保存额外标记信息。
- 读屏障:当应用线程从堆中读取对象引用时,会触发一小段代码。这段代码会检查指针颜色,如果对象正在被移动,则帮助完成指针修复。
- 并发:几乎所有阶段(标记、重定位、重映射)都是并发的。唯一的STW阶段是“暂停所有线程以启动下一个GC周期”,但这个暂停时间与堆大小无关,通常小于1ms。
-
优点:
- 亚毫秒级暂停:STW时间恒定且极短(<1ms)。
- 超大堆支持:可轻松处理从几百MB到16TB的堆。
- 无碎片:使用重定位技术压缩堆。
- 高吞吐量(JDK 15以后):随着优化(如JDK 14引入了指针压缩,JDK 15成为生产就绪),吞吐量已接近G1。
-
缺点:
- CPU负载较高:读屏障在每个引用加载时都会执行,会轻微增加CPU开销(约5%-10%)。
- 内存占用:染色指针需要更多的虚拟地址空间(但其实物理内存占用尚可)。
- JDK版本要求:需要JDK 11+,且最好使用JDK 15+的生产版本。
- 不适用小于4GB的堆:因为收益不明显且有一定开销。
-
适用场景:
- 超低延迟系统:如高频交易、实时分析、在线游戏服务端。
- 巨型内存应用:堆内存 > 64GB,甚至达到1TB级别。
- 要求极其稳定的延迟:不能有任何超过几十毫秒的抖动。
快速对比总结表
| 特性 | Parallel Scavenge / Old | CMS | G1 | ZGC |
|---|---|---|---|---|
| 目标 | 高吞吐量 | 中低延迟 | 平衡延迟与吞吐 | 超低延迟(<1ms) |
| 算法 | 复制 + 标记-整理 | 标记-清除 | 复制 + Region划分 | 染色指针 + 读屏障 |
| STW暂停 | 长(随堆增大而增大) | 中等(但会碎片Full GC) | 可控(默认200ms内) | 极短(<1ms,恒定) |
| 内存碎片 | 无 | 有 | 无 | 无 |
| CPU开销 | 低 | 中等(并发阶段高) | 中等 | 较高 |
| 堆大小建议 | 2GB - 16GB | 4GB - 32GB | 6GB - 64GB+ | 16GB - 16TB |
| JDK版本 | JDK 8默认 | JDK 8可用,9废弃 | JDK 9+默认 | JDK 15+ 生产就绪 |
如何选择?
-
你的应用是批处理/后台计算,不关心偶尔卡顿?
→ Parallel Scavenge / Parallel Old (JDK 8) 或 G1 (JDK 11+ 设为吞吐量优先模式) -
你的应用是Web/微服务,堆 < 8GB,希望暂停 < 100ms?
→ G1 (JDK 9+) 或 Parallel (如果延迟要求不严) -
你的应用堆 8GB - 32GB,对暂停敏感,但无法升级JDK > 8?
→ CMS (但要做好参数调优,并准备接受碎片风险) —— 更建议升级到JDK 11使用G1 -
你的应用要求 GC 暂停 < 10ms,甚至 < 1ms,堆很大(>16GB)?
→ ZGC (JDK 15+) -
如果完全不懂怎么选?
→ 使用你JDK版本的默认GC:- JDK 8:默认Parallel。如果你只写业务代码且没调优过,大概率还是这个。可以考虑主动切换为G1。
- JDK 11/17/21:默认G1。这是最安全、最通用的选择。
最后提醒:GC的选择和调优需要结合监控数据(GC日志、JMX指标)来做。不要盲目追求ZGC,如果你的应用暂停大部分在50ms以下,G1可能已经足够好了。

435

被折叠的 条评论
为什么被折叠?



