Java 面试笔记III · 12 / 14

Lesson 35 · JVM 原理与调优

垃圾收集器全览:Serial → Parallel → CMS → G1 → ZGC

深度·🔥 极高·#JVM·#GC·#核心

第 1 站

面试官:你用过哪些垃圾收集器?它们有什么区别?

"我们线上用的 G1。" 大部分候选人只能说到这一句。如果你能讲清楚五代收集器的演进脉络、每一代要解决的核心痛点、以及它们各自的工程取舍——这道题就是全场最佳回答。

垃圾收集器是 JVM 面试中区分度极高的话题。初级工程师知道"有 GC",中级工程师知道"G1 是默认",而高级工程师能讲清楚为什么会有五代收集器,每一代在吞吐量、延迟、内存 footprint 三者之间做了怎样的取舍

JDK 的垃圾收集器演进史就是一部"如何在停顿时间与吞吐量之间寻找最优解"的工程史:

收集器登场时间核心目标一句话特征
SerialJDK 1.0简单、无额外开销单线程,全程 STW
ParallelJDK 1.4最大化吞吐量多线程 STW,JDK 8 默认
CMSJDK 1.5低停顿时间并发标记-清除,JDK 14 移除
G1JDK 7可预测停顿Region 化堆,Mixed GC,JDK 9+ 默认
ZGCJDK 11亚毫秒停顿染色指针 + 读屏障,JDK 15 生产可用
面试加分项

不要只说"G1 好用"。能说出"CMS 在 JDK 14 被移除,因为它的浮动垃圾和并发模式失败问题在 G1 的 Region 架构下被根本性地解决了"——这才是有深度的回答。本文会帮你建立这种全局视角。

第 2 站

Serial GC:最朴素的单线程回收

Serial GC 是 JVM 最古老的垃圾收集器,也是最简单的一个。它的核心思想只有一句话:用一个线程来完成所有垃圾回收工作,回收期间暂停所有用户线程(Stop-The-World)

单线程做 GC 听起来很低效,为什么它至今仍然存在?

因为"简单"本身就是一种优势。没有多线程切换开销、没有同步屏障、没有内存碎片整理——在堆很小的场景下(比如客户端应用、嵌入式 JVM),Serial GC 反而是效率最高的选择。

工作流程

Serial GC 对新生代和老年代分别使用不同的算法:

  • 新生代(Young Gen):使用 复制算法(Copying)。将 Eden 和 From Survivor 中的存活对象复制到 To Survivor,然后清空 Eden 和 From。这个过程称为 Minor GC
  • 老年代(Old Gen):使用 标记-整理算法(Mark-Compact)。先标记所有存活对象,然后将它们向一端移动压缩,最后清理边界之外的空间。这个过程称为 Major GC / Full GC
启用 Serial GC
# 客户端模式(Client VM)默认启用
java -XX:+UseSerialGC -jar app.jar

# 典型适用场景
# - 堆大小 < 100MB 的小型应用
# - 单核 CPU 环境(如容器内 cpu=1)
# - GraalVM Native Image 默认 GC

Serial GC 的优劣势

维度表现原因
停顿时间差(毫秒~秒级)全程 STW,单线程处理
吞吐量小堆下尚可没有多线程开销
内存开销极低不需要额外的数据结构
适用场景小堆、单核简单即是美
面试话术

Serial GC 是单线程全程 STW 的收集器,没有多线程同步开销,适合小堆和单核环境。在容器化场景下,如果容器只分配了 1 个 CPU 核心,Serial GC 反而可能比 Parallel GC 表现更好,因为避免了线程上下文切换。

第 3 站

Parallel GC:吞吐量之王

Parallel GC(也叫 Throughput Collector)是 JDK 8 的默认垃圾收集器。它的设计目标非常明确:最大化应用程序的吞吐量。做法也很直接——把 Serial GC 的单线程回收改成多线程并行回收。

Parallel 和 Serial 的核心区别是什么?停顿时间会减少吗?

Parallel GC 的多线程回收确实减少了每次 GC 的停顿墙钟时间(wall-clock time),但所有用户线程仍然被暂停(STW)。它的目标不是减少停顿,而是在给定 CPU 资源下最大化应用吞吐量。如果你关心的是"单位时间内处理更多请求",Parallel GC 是对的选择。

新生代:Parallel Scavenge(并行复制)

新生代使用 Mark-Copy 算法,多线程并行完成:

  1. 多线程并行扫描根集合(GC Roots),标记 Eden 和 From Survivor 中的存活对象
  2. 多线程并行将存活对象复制到 To Survivor
  3. 清空 Eden 和 From Survivor

Parallel Scavenge 收集器有一个独特特性:它关注的是吞吐量而非停顿时间,因此提供了 -XX:MaxGCPauseMillis(最大停顿目标)和 -XX:GCTimeRatio(GC 时间占比目标)两个自适应调节参数。

老年代:Parallel Old(并行标记-整理)

老年代使用 Parallel Mark-Compact:多线程并行标记存活对象,然后多线程并行压缩整理。这比 CMS 的标记-清除算法有更好的空间利用率(无碎片),但停顿时间更长。

Parallel GC 调优参数
# 启用 Parallel GC(JDK 8 默认)
java -XX:+UseParallelGC -XX:+UseParallelOldGC \
     -XX:ParallelGCThreads=8          # GC 线程数,默认=CPU 核心数
     -XX:MaxGCPauseMillis=200        # 目标最大停顿时间
     -XX:GCTimeRatio=19              # 目标 GC 时间占比 1/(1+19) = 5%
     -jar app.jar
吞吐量 = 应用运行时间 / (应用运行时间 + GC 时间) × 100%
Parallel GC 的 GCTimeRatio=19 意味着目标吞吐量 = 1/(1+19) = 95%
面试话术

Parallel GC 是 JDK 8 的默认收集器,通过多线程并行回收来最大化吞吐量。新生代用并行 Mark-Copy,老年代用并行 Mark-Compact。它的 STW 时间虽然比 Serial 短(因为多线程),但仍然是全程暂停用户线程的。适合后台计算、批处理等吞吐量敏感场景,不太适合 Web 服务等延迟敏感场景。

第 4 站

CMS:并发标记清除的先驱与困境

CMS(Concurrent Mark-Sweep)是 JDK 1.5 引入的低延迟收集器,在 JDK 5~8 时代是 Web 应用的首选。它的设计目标与 Parallel GC 完全相反:最小化停顿时间。CMS 通过将大部分回收工作与用户线程并发执行来实现这一目标。

为什么"并发"比"并行"更难?

并行是多线程同时做同一件事(比如多线程一起标记),线程之间做的是同一份工作的不同部分。并发是 GC 线程和用户线程同时运行——用户线程在 GC 标记的同时还在修改对象引用关系,这就产生了"我标记了 A 引用 B,但用户线程把 A 改成了引用 C"的一致性问题。CMS 的许多复杂机制都是为了处理这个并发不一致问题。

CMS 的四个阶段

CMS 的老年代回收分为四个阶段,其中两个需要 STW,两个与用户线程并发:

阶段类型做了什么停顿?
1. 初始标记(Initial Mark)STW标记 GC Roots 直接引用的对象是(很短)
2. 并发标记(Concurrent Mark)并发从 GC Roots 出发遍历整个对象图
3. 并发预清理(Concurrent Preclean)并发处理并发标记期间发生变化的引用
4. 最终标记(Final Remark)STW修正并发阶段因用户线程运行而变动的标记是(较长)
5. 并发清除(Concurrent Sweep)并发清除标记为垃圾的对象

CMS 的三大问题

问题一:浮动垃圾(Floating Garbage)

在并发清除阶段,用户线程仍在运行,会产生新的垃圾。这些垃圾只能在下一次 GC 时才能被回收。这意味着 CMS 不能等到堆快满了才启动 GC,需要提前触发——通常当老年代使用率达到 68%(-XX:CMSInitiatingOccupancyFraction)时就开始回收。

问题二:并发模式失败(Concurrent Mode Failure)

如果 CMS 回收的速度赶不上对象分配的速度,老年代空间耗尽,CMS 就会退化为一次 Full GC(Serial Old 单线程标记-整理),停顿时间暴涨到秒级。这是 CMS 最让人头疼的问题。

问题三:内存碎片

CMS 使用标记-清除算法,不会压缩整理老年代,因此会产生大量内存碎片。碎片化严重时,即使总空闲内存足够,也可能因为找不到连续空间来分配大对象而触发 Full GC。

CMS 配置(JDK 9+ 已弃用,JDK 14 已移除)
# JDK 8 中启用 CMS
java -XX:+UseConcMarkSweepGC \
     -XX:CMSInitiatingOccupancyFraction=68  # 老年代占用 68% 时触发
     -XX:+UseCMSInitiatingOccupancyOnly      # 只在达到阈值时触发
     -XX:+CMSScavengeBeforeRemark            # Remark 前先做一次 Minor GC
     -jar app.jar

# JDK 9: -XX:+UseConcMarkSweepGC 产生警告
# JDK 14: 彻底移除 CMS,使用该参数会报错
面试高频追问

Q: CMS 为什么被移除?
三个原因:① 浮动垃圾导致空间利用率低;② 并发模式失败会退化为 Serial Old 单线程 Full GC,停顿不可预测;③ 标记-清除产生内存碎片。这些问题在 G1 的 Region 化架构下得到了根本性解决——G1 使用复制算法天然无碎片,Mixed GC 可以增量回收老年代。

面试话术

CMS 是第一个尝试将 GC 工作与用户线程并发执行的收集器,目标是低停顿。它有四个阶段:初始标记(STW)→ 并发标记 → 并发预清理 → 最终标记(STW)→ 并发清除。但有三个致命问题:浮动垃圾(并发清除期间产生的新垃圾)、并发模式失败(回收速度跟不上分配速度,退化为 Full GC)、内存碎片(标记-清除不压缩)。这些问题促使了 G1 的诞生,CMS 在 JDK 14 被正式移除。

第 5 站

G1:Region 化堆与 Mixed GC

G1(Garbage First)是 JDK 9 以来的默认垃圾收集器。它从架构层面颠覆了前面几代收集器的设计思路:不再将堆划分为固定的新生代和老年代,而是将整个堆划分为大小相等的 Region。每个 Region 可以动态扮演 Eden、Survivor、Old 或 Humongous 角色。

为什么要把堆切成一块一块的 Region?

CMS 最大的问题是 Full GC 停顿不可控——当并发模式失败时,要对整个老年代做单线程标记-整理。G1 的核心洞察是:如果堆被切成独立的 Region,GC 时可以只选择收益最高的 Region(Garbage First 名字由来)进行回收,而不是每次都处理整个堆。这让停顿时间变得可预测。

Region 架构

G1 将堆划分为约 2048 个大小相等的 Region,每个 Region 大小在 1MB~32MB 之间(必须是 2 的幂次方),由堆大小自动决定:

G1 堆布局:Region 化架构(~2048 个 Region) E E E E E E E E E E E E S S H H O O F O O O F O O H H O F O O O F O O O F F F F O O O F O F O O O F O F O O F F F F F ... ~2048 个 Region ... E = Eden(伊甸区) S = Survivor(幸存区) O = Old(老年代) H = Humongous(大对象) F = Free(空闲 Region) Region 关键参数 • Region 大小:1MB ~ 32MB(2 的幂次方),由堆大小自动计算,目标 ~2048 个 Region • Humongous Region:对象大小 > Region 大小的 50% 时,分配在连续的 Humongous Region 中 • 新生代/老年代不再物理隔离:每个 Region 的角色是动态的,GC 后可重新分配 • Remembered Set(RSet):每个 Region 维护一个 RSet,记录其他 Region 对本 Region 的引用 Mixed GC(混合回收) Young GC:回收所有 Eden + Survivor Region | Mixed GC:回收所有年轻代 + 部分收益最高的老年代 Region "Garbage First":优先选择垃圾比例最高的 Region 进行回收,在停顿时间内回收最多的垃圾
图 1 G1 的 Region 化堆布局——堆不再划分为固定的新生代和老年代,而是由 ~2048 个等大的 Region 动态组成

G1 的回收过程

G1 的回收分为两种类型:

Young GC:回收所有 Eden 和 Survivor Region。使用多线程并行复制存活对象到新的 Region(本质上是并行复制算法)。

Mixed GC:在 Young GC 的基础上,额外选择部分老年代 Region 一起回收。G1 通过并发标记周期来确定哪些老年代 Region 的垃圾比例最高(Garbage First),然后选择收益最高的 Region 进行回收。

并发标记周期

G1 的并发标记与 CMS 类似,但更简洁,因为 G1 使用了 Snapshot-At-The-Beginning(SATB) 算法:

  1. 初始标记(STW):标记 GC Roots 直接引用的对象,搭载在 Young GC 的 STW 阶段
  2. 并发标记:与用户线程并发遍历对象图
  3. 最终标记(STW):处理 SATB 队列中的引用变更记录
  4. 筛选回收(STW):计算每个 Region 的回收价值(垃圾比例),选择性价比最高的 Region 组成 Collection Set

Remembered Set(RSet)

因为 Region 的角色是动态的,G1 无法像分代收集器那样只扫描新生代。为了解决跨 Region 引用的问题,每个 Region 都维护一个 RSet,记录"谁引用了我"。这样在 Young GC 时,只需扫描年轻代 Region 的 RSet 就能找到所有引用关系,而不需要扫描整个老年代。

RSet 的代价:每个 Region 的 RSet 通常占用 Region 大小的 5%~20%,这是 G1 内存开销比 Parallel GC 更高的主要原因之一。

G1 核心调优参数
java -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200          # 目标最大停顿时间(默认 200ms)
     -XX:G1HeapRegionSize=4m          # Region 大小(1m/2m/4m/.../32m)
     -XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用率
     -XX:G1NewSizePercent=20          # 新生代最小占比(默认 5%)
     -XX:G1MaxNewSizePercent=60       # 新生代最大占比(默认 60%)
     -XX:ConcGCThreads=2              # 并发标记线程数
     -XX:G1ReservePercent=10          # 保留空间防 evacuation failure
     -jar app.jar

Evacuation Failure(疏散失败)

类似 CMS 的并发模式失败,G1 在 Mixed GC 期间如果没有足够的空闲 Region 来存放存活对象,就会触发 Evacuation Failure,退化为一次 Full GC(单线程标记-整理,JDK 10 后改为多线程)。这是 G1 调优中需要极力避免的情况。

面试话术

G1 将堆划分为约 2048 个等大的 Region,每个 Region 可以动态扮演 Eden/Survivor/Old/Humongous 角色。回收分为 Young GC(只回收年轻代 Region)和 Mixed GC(年轻代 + 部分老年代 Region)。G1 通过"Garbage First"策略,优先选择垃圾比例最高的 Region 回收,使得在用户设定的目标停顿时间(-XX:MaxGCPauseMillis)内回收最多的垃圾。G1 使用 SATB 算法做并发标记,每个 Region 维护 RSet 来记录跨 Region 引用。它是 JDK 9+ 的默认收集器,适合中等大小堆(4GB~64GB)的延迟敏感应用。

第 6 站

ZGC:亚毫秒停顿的终极方案

ZGC(Z Garbage Collector)是 JDK 11 引入的实验性收集器,JDK 15 正式生产可用。它的目标极其大胆:停顿时间不超过 1 毫秒,且不随堆大小增长。无论堆是 8GB 还是 16TB,停顿时间都保持在亚毫秒级别。

G1 已经把停顿控制到 200ms 了,为什么还需要 ZGC?

因为 200ms 对于某些场景来说太长了。高频交易、实时广告竞价、在线游戏服务器——这些场景要求 P99 延迟在个位数毫秒。如果 GC 一次就停顿 200ms,整个 SLA 就崩了。ZGC 的目标是把 GC 停顿压缩到亚毫秒级,让 GC 对应用来说几乎"不可见"。

核心机制一:染色指针(Colored Pointers)

ZGC 最核心的创新是染色指针——利用 64 位指针中的空闲位来存储 GC 元数据。在 64 位系统中,虚拟地址空间远大于实际物理内存,指针的高位通常有多余的 bit。ZGC 利用这些 bit 来存储四个标记:

ZGC 染色指针结构(64-bit) 未使用 63~46 M0 bit 45 Marked0 R bit 44 Remapped F bit 43 Finalizable S bit 42 SoftRef 对象地址(42 bit,最大支持 4TB 堆) bit 41 ~ bit 0 实际物理地址 染色指针的精妙之处 • M0 / M1(Marked0/Marked1):交替使用,标记对象是否在 GC 中被标记为存活 • Remapped:标记指针是否已指向对象的新地址(用于并发转移后的指针修正) • Finalizable:标记对象只能通过 Finalizer 可达(弱引用相关优化) • SoftRef:标记对象只能通过 SoftReference 可达 • 核心优势:不需要额外的 Mark Bitmap 或 Mark Word 来存储 GC 标记,GC 信息直接"染"在指针上
图 2 ZGC 染色指针——利用 64 位指针的空闲位存储 GC 标记信息,将对象元数据嵌入指针本身

核心机制二:读屏障(Load Barrier)

当应用线程通过指针读取对象引用时,ZGC 会在读取前插入一段读屏障代码。读屏障会检查指针的染色位:

  • 如果指针的 Remapped 位被设置,说明对象已经被 GC 转移到了新地址,读屏障会自动修正指针指向新地址
  • 这样就不需要像 G1 那样在 STW 阶段批量修正所有引用——修正工作被分散到了每次指针读取时

核心机制三:并发转移(Concurrent Relocation)

ZGC 将 G1 中必须 STW 的 Evacuation(转移存活对象)阶段也变成了并发的:

  1. GC 线程并发将存活对象从旧 Region 复制到新 Region
  2. 旧地址的指针被标记为 Remapped(染色位)
  3. 应用线程下次通过读屏障访问该指针时,自动修正为新地址
  4. 当所有旧指针都被修正后,旧 Region 可以被安全回收

ZGC 的执行流程

阶段类型说明
Pause Init MarkSTW(亚毫秒)标记 GC Roots 直接引用的对象
Concurrent Mark并发遍历整个对象图
Pause Mark EndSTW(亚毫秒)处理引用队列、软引用等
Concurrent Prepare for Relocate并发构建转移集合
Pause Relocate StartSTW(亚毫秒)转移 GC Roots 引用的对象
Concurrent Relocate并发转移存活对象 + 读屏障自动修正指针

注意:所有 STW 阶段都是亚毫秒级的,最长的 Pause Mark End 通常也不超过 1ms。

启用 ZGC
# JDK 11(实验性)
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar app.jar

# JDK 15+(生产可用,无需 UnlockExperimental)
java -XX:+UseZGC -jar app.jar

# JDK 17+ 启用分代 ZGC(进一步提升吞吐量)
java -XX:+UseZGC -XX:+ZGenerational -jar app.jar

# ZGC 默认最大堆 16TB,适用于超大堆场景

ZGC 的取舍

维度表现说明
停顿时间极好(<1ms)所有 STW 阶段都在亚毫秒级
吞吐量中等偏低读屏障有额外开销,CPU 使用率比 G1 高 ~10%
内存开销较高需要额外的 Region Map、转移表等元数据
适用堆大小8GB ~ 16TB大堆下优势最明显
CPU 要求多核需要较多 CPU 核心给并发 GC 线程
面试话术

ZGC 的核心目标是亚毫秒停顿且不随堆大小增长。它通过三个关键技术实现:① 染色指针——将 GC 标记信息嵌入 64 位指针的空闲 bit 中,避免了额外的元数据结构;② 读屏障——在应用线程读取对象引用时自动修正被转移的指针,将批量修正工作分散化;③ 并发转移——将 G1 中必须 STW 的 Evacuation 阶段变为并发,配合读屏障实现无停顿的对象转移。代价是吞吐量略低于 G1(读屏障开销约 10%),内存开销更大。JDK 15 后生产可用,JDK 17 引入了分代 ZGC 进一步改善吞吐量。

第 7 站

五代收集器全维度对比

维度 Serial Parallel CMS G1 ZGC
停顿时间 毫秒~秒级 毫秒~秒级 10~100ms 10~200ms(可调) <1ms
停顿可控性 不可控 粗略可调 不可控(CMF 退化) 目标值可调 硬保证 <1ms
吞吐量 低(小堆尚可) 最高 中等 中高 中等
内存开销 极低 中(需预留空间) 中高(RSet) 高(染色指针+元数据)
CPU 开销 最低 GC 时高 持续占用 1~2 核 GC 时高 持续占用(读屏障)
堆大小 <100MB <8GB <8GB 4GB~64GB 8GB~16TB
默认 JDK 版本 Client VM JDK 8 无(JDK 14 移除) JDK 9+ JDK 15+(生产可用)
碎片问题 无(Compact) 无(Compact) 严重 无(复制) 无(复制)
浮动垃圾 有(较轻) 有(较轻)
典型场景 嵌入式/小容器 批处理/后台计算 Web 服务(已淘汰) 通用 Web 服务 超低延迟/大堆

GC 演进时间线

垃圾收集器演进时间线 1 Serial JDK 1.0 1996 2 Parallel JDK 1.4 2002 3 CMS JDK 1.5 2004 4 G1 JDK 7(9+默认) 2011 / 2017 5 ZGC JDK 11(15+生产) 2018 / 2020 JDK 14 移除 2020 演进主线:从"暂停一切"到"几乎不停" Serial → Parallel:单线程 → 多线程,提升吞吐量(目标:快) Parallel → CMS:STW → 并发,降低停顿时间(目标:稳) CMS → G1:固定分代 → Region 化,解决碎片 + 不可控停顿(目标:可控) G1 → ZGC:STW 转移 → 并发转移 + 染色指针,亚毫秒停顿(目标:极致) 每一次演进都在回答同一个问题:如何让 GC 对应用的影响更小?
图 3 垃圾收集器演进时间线——从 1996 年的 Serial 到 2020 年生产可用的 ZGC,核心主线是降低停顿时间
Shenandoah 和 Epsilon

JDK 中还有两个值得一提的收集器:Shenandoah(Red Hat 开发,目标类似 ZGC 的低停顿,但不使用染色指针而是用 Brooks Pointer 转发)和 Epsilon(不做任何回收,用于性能测试和短生命周期应用)。面试中提到它们可以作为加分项,但不需要深入展开。

第 8 站

你的项目该用哪个收集器?

选择决策树

GC 选择指南
/*
 * 第一步:你的堆有多大?
 */
if (heapSize < 100MB && cpuCores == 1) {
    return Serial;           // 小堆单核,简单就是美
}

if (heapSize < 4GB && 吞吐量优先) {
    return Parallel;         // 批处理、后台任务
}

if (heapSize < 64GB && 通用场景) {
    return G1;               // JDK 9+ 默认,大多数场景的最优选
}

if (延迟敏感 || heapSize > 16GB || P99要求 < 10ms) {
    return ZGC;              // 超低延迟,JDK 15+
}

// 默认:用 G1,别折腾
return G1;

生产环境 GC 监控指标

指标含义健康范围告警阈值
GC Pause Time单次 GC 停顿时长G1: <200ms / ZGC: <1ms超过目标值 2 倍
GC FrequencyGC 触发频率Young: 每几分钟一次每分钟超过 1 次
GC Time RatioGC 时间占总时间比<5%>10%
Full GC 次数Full GC 触发次数0(不应发生)任何 Full GC 都要告警
Heap Usage Trend堆使用率趋势锯齿状波动持续上升不回落
JVM GC 日志关键参数(JDK 9+ 统一日志框架)
# JDK 9+ 统一日志
java -Xlog:gc*,gc+heap=debug:file=gc-%t.log:time,uptime,level,tags:filecount=5,filesize=10M

# JDK 8 传统参数
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps \
     -Xloggc:/var/log/gc-%t.log \
     -XX:+UseGCLogFileRotation \
     -XX:NumberOfGCLogFiles=5 \
     -XX:GCLogFileSize=10M

面试官问"你们线上用的什么 GC?怎么调优的?"怎么回答?

好的回答模板:"我们线上用的是 G1 / ZGC,堆大小 X GB。核心关注三个指标:单次停顿时间不超过 N ms(通过 -XX:MaxGCPauseMillis 设定)、GC 时间占比低于 5%(通过 GC 日志分析)、不发生 Full GC。曾经遇到过 Mixed GC 不及时导致的 Evacuation Failure,通过调低 InitiatingHeapOccupancyPercent 从 45 到 35 解决,让并发标记更早启动。"

全文核心记忆点:

  • Serial:单线程全程 STW,适合小堆单核
  • Parallel:多线程 STW,吞吐量最高,JDK 8 默认
  • CMS:并发标记-清除,低停顿但有浮动垃圾、并发模式失败、内存碎片三大问题,JDK 14 移除
  • G1:Region 化堆 + Mixed GC + SATB,可预测停顿,JDK 9+ 默认,适合大多数场景
  • ZGC:染色指针 + 读屏障 + 并发转移,亚毫秒停顿,JDK 15 生产可用,适合大堆/超低延迟
最终面试话术

JDK 的 GC 演进是一条"降低停顿"的主线:Serial 全程 STW → Parallel 多线程缩短 STW → CMS 将标记与用户线程并发 → G1 用 Region 化 + 增量回收实现可预测停顿 → ZGC 用染色指针和读屏障将停顿压到亚毫秒。每一代都是在吞吐量、延迟、内存三者之间的重新取舍。生产环境大多数情况用 G1,对延迟有极致要求或堆超过 16GB 时选 ZGC。