欢迎光临
我们一直在努力

(JVM _02)JVM垃圾回收器深度解析

JVM 垃圾回收器深度解析:Parallel、CMS 与 G1

从吞吐量到延迟,从整堆回收到分区增量——看懂这三款主流 GC 的设计哲学与选型之道

背景

Java 开发者迟早会面对一个经典问题:我的应用应该用哪种垃圾回收器? 在 JDK 8 时代,Parallel GC 和 CMS 是两大主力;进入 JDK 9+,G1 成为了默认选择。 不同回收器在吞吐量与响应时间之间做出了截然不同的权衡,理解它们的内部机制,是写出高性能 Java 服务的基础。

目的

本文旨在:

  • 理清 Parallel GC、CMS、G1 三者的核心设计原理
  • 对比它们的基础概念(内存布局、GC 阶段、停顿模式)
  • 归纳各自的适用场景与版本演进
  • 以 问-答 形式提炼关键知识点,便于理解与回顾

目录

  • 吞吐量与响应时间:GC 调优的本质
  • Parallel GC:吞吐量优先的整堆回收器
  • CMS:并发标记清除的先行者
  • G1:分区增量回收的集大成者
    • 4.1 Region —— 物理分区,逻辑角色
    • 4.2 期望时间 —— 给 GC 设一个 DDL
    • 4.3 RSet 与写屏障 —— 跨 Region 引用的精确记录
    • 4.4 大对象(Humongous)—— 超过 50% Region 的特殊处理
  • 三款 GC 核心对比表
  • 版本演进与选型建议
  • 重点问答

  • 1. 吞吐量与响应时间:GC 调优的本质

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • 吞吐量 = 业务运行时间 / (业务运行时间 + GC 总时间) 追求高吞吐意味着希望 GC 花费的时间占比尽可能小。
    • 响应时间 = 单次 GC 停顿的时长 追求低延迟意味着每次 GC 暂停必须很短(例如 < 100ms)。

    为何二者不能兼得? GC 停顿时长与频率成反比:

    • 要缩短单次停顿,GC 必须更频繁地工作(并发、增量),这会引入额外的管理开销,降低吞吐量。
    • 要最大化吞吐量,GC 必须尽可能减少 GC 次数,但这导致每次 GC 需要回收更多内存,单次停顿时间变长。

    调优的本质:根据业务场景,在可接受的响应时间下,尽量提高吞吐量,或反之。


    2. Parallel GC:吞吐量优先的整堆回收器

    • 别名:吞吐量优先收集器
    • 核心设计:多线程 + Stop-The-World(STW),整堆回收
    • 内存布局:连续的 Eden、Survivor(S0/S1)、Old 区
    • GC 阶段:
      • Young GC:Eden → Survivor(复制,STW)
      • Full GC:整堆标记-压缩(STW,多线程)
    • 调优参数:-XX:+UseParallelGC(JDK 8 默认),-Xmn,-XX:SurvivorRatio 等
    • 优点:GC 总耗时最少,吞吐量最高
    • 缺点:单次停顿不可控,大堆下可能 STW 数秒
    • 适用场景:批处理、离线计算、报表任务(可容忍长暂停)

    3. CMS:并发标记清除的先行者

    • 别名:并发标记清除收集器
    • 核心设计:标记-清除算法,大部分阶段与用户线程并发执行
    • 内存布局:连续整块(与 Parallel 类似)
    • GC 阶段:
      • 初始标记(STW,很短)
      • 并发标记(与用户线程并行)
      • 重新标记(STW)
      • 并发清除(与用户线程并行)
    • 致命缺陷:
      • 内存碎片:不整理,最终导致大对象无法分配 → Full GC(Serial,极慢)
      • 浮动垃圾:并发清除时新产生的垃圾只能下次处理
      • Concurrent Mode Failure:并发阶段 Old 区满,降级为 Serial Full GC
    • 状态:JDK 9 标记废弃,JDK 14 彻底移除
    • 适用场景:JDK 8 及更早版本中,对延迟有一定要求且堆不太大的在线服务(现已不推荐)

    4. G1:分区增量回收的集大成者

    G1(Garbage-First)从 JDK 7 引入,JDK 9 起成为默认 GC。它通过分区(Region)与增量回收,实现了可预测的停顿时间。

    4.1 Region —— 物理分区,逻辑角色

    • 物理上:堆被分成多个大小相等的 Region(默认 1~32MB,约 2048 个)。
    • 逻辑上:每个 Region 在某个时刻扮演 Eden、Survivor、Old 或 Humongous 中的一种角色,且角色可动态变化。

    堆内存(8GB,2048 个 Region)
    ┌────┬────┬────┬────┬────┬────┬────┬────┐
    │ E │ E │ S │ O │ H │ O │ E │ O │ …
    └────┴────┴────┴────┴────┴────┴────┴────┘
    E = Eden, S = Survivor, O = Old, H = Humongous

    • 优势:无需手动设置年轻代大小(-Xmn),G1 根据停顿目标动态调整各角色 Region 数量。

    4.2 期望时间 —— 给 GC 设一个 DDL

    • 参数:-XX:MaxGCPauseMillis=200(默认 200ms)
    • 工作原理:G1 会根据这个目标,动态决定每次 Young GC 或 Mixed GC 回收多少个 Region。如果实际停顿超过目标,下次就回收更少的 Region(同时降低年轻代大小);如果远低于目标,则增加回收量,提高吞吐量。
    • 效果:GC 停顿时间变得可预测且可控。

    4.3 RSet 与写屏障 —— 跨 Region 引用的精确记录

    问题:G1 只回收部分 Region,如何知道其他 Region 是否指向本 Region 的对象?

    答案:RSet(Remembered Set) —— 每个 Region 维护一个哈希表,记录“哪些 Region 中的对象引用了本 Region 的对象”。

    • 维护方式:通过写屏障(Write Barrier)。当执行 obj1.field = obj2 且两对象位于不同 Region 时,写屏障会在目标 Region 的 RSet 中添加一条记录。
    • GC 时如何使用:回收 Region X 时,只需扫描 X 的 RSet 中所列出的那些 Region,找出根引用,无需全堆扫描。

    代价:额外内存(RSet 约占 5%~10% 堆)和 CPU 开销(写屏障),导致吞吐量略低于 Parallel GC。

    4.4 大对象(Humongous)—— 超过 50% Region 的特殊处理

    • 定义:对象大小 > Region 大小的一半(例如 Region=2MB,对象>1MB)。
    • 分配:直接分配在连续的 Humongous Region 中(不经过 Eden/Survivor)。
    • 回收:只在 Mixed GC 或 Full GC 时回收,不参与 Young GC。
    • 风险:大对象分配过快会快速占满 Humongous 区,触发 Full GC(JDK 8 单线程,极慢)。
    • 调优:可增大 Region 大小(-XX:G1HeapRegionSize=32MB),或从代码层面避免超大对象。

    5. 三款 GC 核心对比表

    维度Parallel GCCMSG1
    内存布局 连续整块(Eden/S0/S1/Old) 连续整块 分区(Region,动态角色)
    GC 阶段 全程 STW 大部分并发 增量 STW(可控)
    内存整理 有(复制+压缩) 无(标记清除→碎片) 有(增量复制)
    跨代引用 卡表(Card Table) 卡表+写屏障 RSet+写屏障
    单次停顿 长(不可控) 较短(但不可控) 短且可控(MaxGCPauseMillis)
    吞吐量 最高 较低 中等(略低于 Parallel)
    年轻代调参 需手动 -Xmn 需手动 自适应(无需设置)
    Full GC 多线程并行 单线程 Serial(极慢) JDK8 单线程 / JDK10+ 并行
    适用 JDK 版本 JDK 8 默认 JDK 14 已移除 JDK 9+ 默认

    6. 版本演进与选型建议

    JDK 版本ParallelCMSG1
    JDK 8 默认 可用(但废弃) 需 -XX:+UseG1GC(8u40+ 生产可用)
    JDK 9~10 可用 废弃(警告) 默认(Full GC 仍单线程)
    JDK 11 可用 已移除 默认,Full GC 单线程
    JDK 17+ 可用 已移除 默认,Full GC 并行(JDK 10+ 已改进)

    选型建议:

    • 批处理/离线任务:Parallel GC
    • JDK 8 在线服务(堆 4-8GB):G1(显式启用)
    • JDK 9+ 在线服务:G1(默认,无需参数)
    • 超大堆(>64GB)且要求极低延迟:ZGC(JDK 11+ 实验,15+ 生产)

    7. 重点问答

    问:G1 中的 Region 与传统 GC 的 Eden/Survivor/Old 是什么关系? 答: Region 是物理内存格子,Eden/Survivor/Old 是逻辑角色。每个 Region 在某一时刻扮演其中一种角色,且角色可以动态变化。G1 不再需要固定的年轻代大小。

    问:为什么 G1 能控制 GC 停顿时间? 答: 通过 -XX:MaxGCPauseMillis 设定期望值,G1 会动态调整每次回收的 Region 数量——目标越小,每次回收的 Region 越少,停顿越短,但 GC 频率升高。

    问:什么是 RSet?为什么要用写屏障来维护它? 答: RSet 是每个 Region 的记录表,存放“哪些外部 Region 引用了本 Region 的对象”。写屏障在每次跨 Region 引用赋值时触发,负责更新目标 Region 的 RSet。这样 GC 回收时只需扫描 RSet 中的 Region,避免了全堆扫描。

    问:G1 如何处理大对象?有什么风险? 答: 超过 Region 大小一半的对象称为大对象,直接分配在连续的 Humongous Region 中。它们不参与 Young GC,只在 Mixed GC 或 Full GC 时回收。如果频繁分配大对象,容易占满 Humongous 区,触发 Full GC(尤其在 JDK 8 中可能是单线程的,导致极长停顿)。

    问:CMS 为什么会被移除? 答: 主要三个致命缺陷:内存碎片(最终导致 Full GC)、浮动垃圾(并发模式失败)、无法精确控制停顿。G1 在 JDK 9 之后已完全覆盖并超越了 CMS 的应用场景。

    问:Parallel GC 还在什么场景下使用? 答: 离线批处理、数据统计、科学计算等对响应时间不敏感,只追求最大吞吐量的场景。Parallel GC 的 GC 总耗时占比最小。

    问:JDK 8 高版本(8u40+)到底能不能用 G1? 答: 能。JDK 8u40 完成了并发类卸载,G1 已生产可用。需要显式添加 -XX:+UseG1GC,且 Full GC 仍是单线程的。如果条件允许,升级到 JDK 17 会得到更成熟的 G1(并行 Full GC)。


    本文由 AI 整理,结合了 Java 社区公开资料与主流实践认知。

    赞(0)
    未经允许不得转载:171主机测评 » (JVM _02)JVM垃圾回收器深度解析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址