JVM 垃圾回收器(Garbage Collector, GC)负责自动管理 Java 程序运行过程中产生的内存垃圾(不再被引用的对象),回收其占用的内存空间,防止内存泄漏并确保应用程序的稳定运行。不同的垃圾回收器采用了不同的算法和策略来平衡吞吐量(应用程序执行时间占总时间的比例)、停顿时间(Stop-The-World, STW - 暂停应用程序线程进行垃圾回收的时间)和内存占用。
以下是主要的 JVM 垃圾回收器及其对应的核心垃圾回收策略(截至 Java 17):
一、经典分代收集器 (基于分代假说)
JVM 堆内存通常划分为年轻代 (Young Generation) 和老年代 (Old Generation/Tenured Generation)。大部分对象生命周期很短(朝生夕死),适合在年轻代快速回收;存活下来的对象会逐渐晋升到老年代,老年代的对象回收频率较低但回收成本较高。
-
Serial GC (
-XX:+UseSerialGC
)- 策略: 单线程收集器。
- 年轻代算法: 标记-复制 (Mark-Copy)。将 Eden 区和存活的一个 Survivor 区的对象复制到另一个空的 Survivor 区(或直接晋升到老年代),然后清空 Eden 和原来的 Survivor。
- 老年代算法: 标记-整理 (Mark-Compact)。标记存活对象,然后将它们向内存空间的一端移动(整理),清理掉边界外的所有内存(即垃圾)。
- 特点: 简单高效,没有线程交互开销。但进行垃圾回收时,会暂停所有应用线程 (STW)。适用于单核 CPU、客户端模式或对停顿时间不敏感的微服务/小型应用。
-
Parallel GC / Throughput GC (
-XX:+UseParallelGC
)- 策略: 多线程并行收集器,目标是最大化吞吐量。
- 年轻代算法: 并行标记-复制 (Parallel Mark-Copy)。利用多线程并行执行年轻代的垃圾回收工作,加速复制过程。
- 老年代算法: 并行标记-整理 (Parallel Mark-Compact)。同样利用多线程并行执行老年代的标记和整理工作。
- 特点: 显著提升了垃圾回收速度(相比 Serial),减少了 STW 时间(在多核环境下)。是 JDK 8 及之前版本的默认垃圾回收器。适合后台运算、科学计算等对吞吐量要求高、能容忍中等停顿时间(可能几百毫秒)的应用。
-
ParNew GC (
-XX:+UseParNewGC
)- 策略: Serial GC 的多线程版本,专门用于年轻代。需要与 CMS 搭配使用。
- 年轻代算法: 并行标记-复制 (Parallel Mark-Copy)。原理同 Parallel GC 的年轻代。
- 老年代搭档: 必须配合 CMS 使用。
- 特点: 在年轻代回收上比 Serial 更快(多核)。主要用于作为 CMS 的年轻代搭档。在 JDK 9 后被标记为废弃,因为 G1 成为默认且 CMS 本身也被废弃。
-
CMS (Concurrent Mark-Sweep) GC (
-XX:+UseConcMarkSweepGC
- JDK 14 被移除)- 策略: 以获取最短停顿时间为目标的收集器,特别是针对老年代。其核心是并发标记和清除,尽可能减少 STW 时间。
- 年轻代算法: 通常搭配 ParNew (并行标记-复制)。
- 老年代算法: 并发标记-清除 (Concurrent Mark-Sweep)。过程复杂:
- 初始标记 (Initial Mark - STW): 标记 GC Roots 直接关联的对象,速度快。
- 并发标记 (Concurrent Mark): GC 线程与应用线程并发执行,遍历对象图进行可达性分析。
- 重新标记 (Remark - STW): 修正并发标记期间因应用线程运行导致标记变动的部分。比初始标记长,但远短于并发标记。
- 并发清除 (Concurrent Sweep): GC 线程与应用线程并发执行,清除未被标记(即垃圾)的对象。
- 特点:
- 老年代回收的大部分工作(标记和清除)与应用线程并发执行,显著减少了老年代回收的 STW 时间。
- 采用标记-清除 (Mark-Sweep),会产生内存碎片。当碎片过多导致无法分配大对象时,会触发一次 Serial Old GC(单线程的标记-整理)进行碎片整理,此时停顿时间会很长。
- 对 CPU 资源敏感,并发阶段会占用一部分 CPU 资源,可能影响应用吞吐量。
- 无法处理浮动垃圾 (Floating Garbage):在并发清除阶段应用线程产生的垃圾,只能留到下一次 GC 处理。
- JDK 9 被标记为废弃,JDK 14 中被移除。主要被 G1 和 ZGC/Shenandoah 取代。
二、新一代垃圾回收器 (面向低延迟)
-
G1 (Garbage-First) GC (
-XX:+UseG1GC
- JDK 9+ 默认)- 策略: 面向服务端应用,兼顾吞吐量和低停顿时间。核心思想是将堆划分为多个大小相等的 Region,每个 Region 可以是 Eden, Survivor, Old 或 Humongous(存放大对象)区。
- 算法: 标记-整理 (Mark-Compact) 为主,但在局部 Region 之间采用复制 (Copy) 算法。
- 回收过程 (Mixed GC):
- 年轻代回收 (Young GC - STW): 对 Eden 和 Survivor Region 进行回收(标记-复制),存活对象复制到新的 Survivor Region 或晋升到 Old Region。
- 并发标记周期 (Concurrent Cycle): 类似 CMS,但不分代全局标记。
- 初始标记 (STW - 依附于 Young GC)。
- 根区域扫描。
- 并发标记。
- 最终标记 (STW)。
- 清理 (STW + 并发):统计各 Region 的存活对象和回收价值(Garbage-First 名字来源),选择回收价值高的 Region 放入 Collection Set。
- 混合回收 (Mixed GC - STW): 对 Collection Set 中的 Region(包含年轻代和老年代 Region)进行回收,采用复制算法将存活对象复制到空闲 Region(相当于整理)。
- Full GC (Serial Full GC - 后备): 当回收速度跟不上对象分配速度或无法找到足够连续空间时触发,退化使用 Serial Old GC 进行整个堆的标记-整理(应尽量避免)。
- 特点:
- 可预测的停顿时间模型 (
-XX:MaxGCPauseMillis
):通过控制每次回收的 Region 数量来大致控制停顿时间。 - 并行与并发结合:充分利用多核优势,部分工作与应用线程并发执行。
- 分 Region 管理:按 Region 回收,优先回收价值高的 Region(垃圾最多)。
- 整体标记-整理,局部复制:有效避免了 CMS 的内存碎片问题。
- JDK 9 及以后版本的默认垃圾回收器,适用性非常广。
- 可预测的停顿时间模型 (
-
ZGC (Z Garbage Collector) (
-XX:+UseZGC
)- 策略: 超低延迟 垃圾回收器,目标是将 STW 停顿时间控制在 10ms 以内,且与堆大小无关(TB 级堆也能保持低停顿)。适用于对延迟极其敏感的应用。
- 核心技术:
- 染色指针 (Colored Pointers): 在指针中嵌入元数据(标记位、重映射标记位等),替代传统的内存对象头存储标记信息。
- 读屏障 (Load Barrier): 在应用线程从堆中加载引用时触发一小段代码,配合染色指针实现并发处理(如标记、重定位)。
- 算法: 基于 Region 的并发标记-整理。
- 回收阶段 (几乎全并发):
- 并发标记 (Concurrent Mark): 遍历对象图标记可达对象。
- 并发预备重分配 (Concurrent Prepare for Relocation): 确定需要清理(重定位)的 Region。
- 并发重分配 (Concurrent Relocation): 核心阶段。将存活对象复制到新的 Region,并通过读屏障和应用线程协作逐步更新所有指向这些对象的引用(自愈指针)。
- 并发重映射/标记 (Concurrent Remap/Remark): 修正重分配期间漏掉的引用(较少发生)。
- 特点:
- 超低停顿时间 (Sub-millisecond to ~10ms):STW 阶段仅剩初始标记和最终标记中的根扫描部分(极短)。
- 高吞吐量损失相对小:虽然并发操作消耗资源,但设计优秀,吞吐量损失比 CMS 小。
- 大堆友好:停顿时间不随堆大小增长而增加。
- 支持压缩/整理:消除碎片。
- JDK 15 正式发布生产可用,仍在快速发展中。
-
Shenandoah GC (
-XX:+UseShenandoahGC
)- 策略: 与 ZGC 目标类似,追求低停顿时间(通常 <10ms) 且与堆大小无关。
- 核心技术与 ZGC 异同:
- 转发指针 (Brooks Pointer / Forwarding Pointer): 在每个对象头中增加一个额外的“转发指针”字段。在对象被复制后,旧位置的转发指针指向新位置。
- 读屏障 (Load Barrier) 和 写屏障 (Write Barrier): 都需要。读屏障用于解决并发引用加载问题(类似ZGC),写屏障用于捕获引用更新(确保在并发移动时引用的正确性)。
- 算法: 基于 Region 的并发标记-复制/整理。
- 回收阶段 (几乎全并发):
- 初始标记 (Initial Mark - STW):标记根。
- 并发标记 (Concurrent Mark)。
- 最终标记 (Final Mark - STW):处理剩余的 SATB(Snapshot-At-The-Beginning)引用。
- 并发清理 (Concurrent Cleanup):清理无存活对象的 Region。
- 并发回收 (Concurrent Evacuation): 核心差异点。并发地将存活对象复制到新的 Region。应用线程在访问对象时,通过读/写屏障感知并处理对象移动和引用更新。
- 初始引用更新 (Init Update Refs - STW):短暂暂停确保所有线程看到回收开始前的状态。
- 并发引用更新 (Concurrent Update References): 遍历堆,更新指向已移动对象的引用。
- 最终引用更新 + 清理 (Final Update Refs + Cleanup - STW):完成引用更新并清理。
- 特点:
- 超低停顿时间:目标与 ZGC 一致。
- 与 ZGC 性能接近:两者在低延迟方面表现都非常出色,具体选择可能取决于具体应用场景、JVM 版本和平台优化。
- 更早进入 OpenJDK (JDK 12),但最初由 Red Hat 主导。现已是 OpenJDK 主流 GC 之一。
总结对比表
垃圾回收器 | 目标 | 年轻代算法 | 老年代算法 | 线程模式 | 主要特点/适用场景 | STW 时间 | 碎片 | JDK 状态 |
---|---|---|---|---|---|---|---|---|
Serial | 单核简单 | 标记-复制 (单) | 标记-整理 (单) | 单线程 STW | 客户端模式, 资源受限 | 长 | 无 | 可用 |
Parallel/Throughput | 高吞吐量 | 标记-复制 (并) | 标记-整理 (并) | 多线程并行 STW | 后台计算, 批处理, JDK8 默认 | 中等 | 无 | 可用, JDK8 默认 |
ParNew | 配合CMS年轻代 | 标记-复制 (并) | 需搭配 CMS | 多线程并行 STW | CMS 的年轻代搭档 | 年轻代短 | - | 废弃 (JDK9+) |
CMS | 低停顿 (老年代) | (通常 ParNew) | 并发标记-清除 | 并发+并行 | 老年代低停顿, Web应用 (历史), 内存碎片, CPU敏感 | 老年代短 (除碎片整理) | 有 | 移除 (JDK14+) |
G1 | 吞吐量+可控停顿 | 复制 (Region) | 标记-整理+复制 (Region) | 并发+并行 | 通用服务端, JDK9+ 默认, 可预测停顿, Region, Mixed GC | 可控 (可设置) | 极少 | JDK9+ 默认 |
ZGC | 超低停顿 | 无分代 (Region) | 并发标记-整理 (染色指针) | 并发为主 | 极低延迟 (10ms), 大堆, 染色指针, 读屏障 | 极短 (亚毫秒) | 无 | 生产可用 (JDK15+) |
Shenandoah | 超低停顿 | 无分代 (Region) | 并发标记-复制/整理 (转发指针) | 并发为主 | 极低延迟 (10ms), 大堆, 转发指针, 读/写屏障 | 极短 | 无 | 生产可用 (JDK12+) |
选择建议
- 吞吐量优先 (CPU 密集型):
Parallel GC
(或 G1 调优后)。 - 中等延迟要求 (通用服务端):
G1 GC
(JDK9+ 默认,平衡性最好)。 - 极低延迟要求 (延迟敏感型应用,如金融交易、实时系统):
ZGC
或Shenandoah GC
。 - 小内存/客户端/嵌入式:
Serial GC
。 - 避免使用:
CMS
(已移除),ParNew
(废弃)。
重要参数
-XX:+Use<GCName>GC
: 启用特定 GC (e.g.,-XX:+UseG1GC
,-XX:+UseZGC
)。-Xms
/-Xmx
: 设置堆的初始大小和最大大小。-XX:NewRatio
: 年轻代与老年代的比例 (e.g.,2
表示 老年代:年轻代=2:1)。-XX:SurvivorRatio
: Eden 区与一个 Survivor 区的比例 (e.g.,8
表示 Eden:S0:S1=8:1:1)。- G1:
-XX:MaxGCPauseMillis
: 期望的最大 GC 停顿时间目标 (毫秒, 默认 200ms)。-XX:G1HeapRegionSize
: 设置 Region 大小 (1MB-32MB, 2的幂)。
- ZGC/Shenandoah: 通常自动调优,也可设置堆大小等基础参数。
理解不同垃圾回收器的原理和适用场景,结合应用的具体需求(延迟、吞吐量、堆大小)进行选择和调优,是保证 Java 应用性能稳定的关键环节。