一个 Java 服务可能大部分时候响应平稳,却偶尔有一批请求一起变慢。垃圾回收暂停是可能的原因之一:应用本来可以继续执行,却需要等待回收器完成某段工作。服务的堆越大、存活对象越多,这段等待就越难靠“让 GC 再快一点”解决。
垃圾回收器的演进,围绕的正是这些具体成本。能否用更多线程缩短回收时间?能否只处理一部分堆?能否让对象一边移动,应用一边继续访问?从 JDK 8 中已经存在的几类回收器,到后来的 ZGC、Shenandoah,再到分代低停顿回收,这些问题把技术变化连成了一条可以理解的主线。
读完可以掌握
- GC 如何判断对象是否存活,为什么整理内存会牵涉暂停、引用更新和并发一致性。
- Serial、Parallel、CMS、G1、ZGC 与 Shenandoah 各自缓解了什么瓶颈,又把成本留在了哪里。
- 从 JDK 8 到 JDK 27,回收器的引入、正式交付和默认模式变化,分别意味着什么。
本文以 HotSpot 中的回收器为主,版本时间采用 OpenJDK 主线交付节点,历史范围从 JDK 8 开始,截至 2026 年 9 月。发行版回移植某项能力的时间可能不同。各条技术路线长期并存;后出现的回收器,并不意味着前一种面对的需求已经消失。
阅读导航
先建立地图:GC 必须完成哪几件事
垃圾回收首先要找出仍然可能被程序使用的对象,然后回收其余空间。为了让后续分配更方便,它还可能移动存活对象。前两句话很短,真正的复杂性集中在“应用同时还在改变这些对象”上。
从可达性开始,而不是数对象被引用了几次
HotSpot 中这些回收器主要采用追踪式回收:从 GC Roots 出发,沿引用关系寻找仍然可达的对象。根集合包含执行线程和运行时持有的必要引用,例如线程执行状态中的对象引用。下面是教学示意,省略不同根来源的实现细节:
GC Roots ├─→ 当前订单 ─→ 订单明细 └─→ 缓存入口 ─→ 配置对象 临时对象 A ←→ 临时对象 B ↑ 没有来自根集合的可达路径
订单和配置可以沿根找到,需要保留。A、B 即使互相引用,只要这组对象不再能从根到达,也可以成为回收对象。因此,循环引用本身不会让追踪式 GC 永远无法释放它们。
找到垃圾之后,空间可以用不同方式回收。标记后清除把无用空间交回分配器,但存活对象之间可能留下孔洞。复制或压缩整理会移动存活对象,把空闲空间集中起来;移动的代价主要与需要处理的存活数据和引用有关,不能只看“这次垃圾很多”。基础机制:Oracle 垃圾回收教程
移动对象,等于增加了一道一致性问题
假设对象从地址 A 搬到地址 B,某个字段中却还保存着 A。运行时必须保证应用得到有效对象,并协调复制期间发生的读写,不能让应用访问已经失效的位置,也不能丢失应用刚写入的数据。
暂停应用是最直接的协调方式:对象关系暂时稳定,回收器完成搬迁和引用更新后,再让应用继续。并发移动则需要更复杂的协议,使回收器和应用在对象地址变化时仍能合作。后来的低停顿回收器,主要就在这里向前推进。
STW、并行、并发描述的是不同维度
STW,也就是 Stop The World,指应用的 Java 执行线程在安全位置暂停,等待某段运行时工作完成。GC 线程仍然可以运行,操作系统和其他进程也不会一起停止。
并行回收使用多个 GC 线程完成工作,应用在相应阶段仍可能全部暂停。并发回收则允许 GC 与应用在一部分时间内同时运行。一个回收器完全可以既有并行阶段,又有并发阶段。
单线程暂停回收:应用运行 | GC 线程独自工作,应用暂停 | 应用恢复 多线程暂停回收:应用运行 | 多个 GC 线程共同工作 | 应用恢复 并发回收: 应用运行 | 应用与 GC 同时工作 | 应用继续 ↑ 阶段切换仍可能有短暂停
吞吐量关注一段时间内完成了多少业务工作,最长暂停关注某一次应用被迫等了多久。用多个线程迅速完成一次集中回收,可能具有很好的吞吐量,却仍留下明显的单次停顿;把工作分散到并发阶段,可能缩短停顿,但也会持续占用应用本来可以使用的 CPU。
分代减少重复劳动,屏障补上遗漏的信息
分代回收利用对象寿命的差异。很多临时对象很快就失去用途,长寿命对象却会跨过多次回收。如果每次都重新检查大量长寿命对象,便会浪费工作。于是,回收器可以更频繁地处理年轻代,较少处理老年代。来源:JDK 8 分代回收说明
但只看年轻代也会漏掉入口:一个年轻对象可能仅被老年代对象的字段引用。因此,运行时需要记录老年代指向年轻代等必要关系。应用写入对象引用时执行的一小段附加逻辑,可以帮助维护这些信息,这就是 GC 写屏障的一种用途。
GC 屏障也可以承担并发标记和对象搬迁的协作任务。它是加在特定引用操作附近的运行时逻辑,不能简单理解为一把锁,也不能与处理器上约束指令顺序的内存屏障直接画等号。
有了这几个概念,后续技术可以放回同一张地图:分代和分区缩小处理范围,并行缩短集中工作的时间,并发减少必须在暂停里完成的工作,屏障则让这些变化保持正确。
JDK 8 的起点:几条路线已经同时存在
JDK 8 已经包含 Serial、Parallel、CMS 和 G1。这里从它们当时的组合出发,不把 JDK 8 当作它们的诞生时间。
| JDK 8 中的路线 | 年轻代与老年代的关系 | 主要方向 |
|---|---|---|
| Serial / Serial Old | 年轻代 Serial,老年代 Serial Old | 用较简单的单线程暂停回收完成内存管理 |
| Parallel Scavenge / Parallel Old | 年轻代与老年代均可并行回收 | 利用多个核心,提高整体吞吐量 |
| ParNew / CMS | ParNew 处理年轻代,CMS 处理老年代 | 将老年代的部分工作与应用并发执行 |
| G1 | 统一管理按 Region 划分的堆,仍保留分代 | 在回收收益与暂停预算之间选择处理范围 |
JDK 8 的服务器级配置通常默认使用 Parallel GC,Parallel Old 已是其默认老年代配套。G1 当时可以使用,但尚未接过这一默认位置。来源:JDK 8 回收器说明、Parallel GC 文档
Serial 与 Parallel:先把暂停里的工作做得更快
Serial 展示了分代回收的一种直接实现。年轻代中的大部分对象如果已经死亡,就只需把少量存活对象复制到幸存区域,或在满足条件时晋升到老年代,随后整体回收原先使用的区域。它不需要逐一搬走垃圾,而是把还需要保留的对象带走。
年轻代回收前:[新对象][已死][已死][存活对象][已死] ↓ 复制 回收后: [原区域重新可用] [幸存区域 / 老年代]
老年代往往保存更多长寿命对象,Serial Old 使用标记和压缩整理,把存活对象集中起来,减少碎片。应用需要等待这些工作完成;单个回收线程也无法把多核机器的能力充分用于这段暂停。
Parallel 路线把更多 GC 工作交给多个线程共同完成。它仍然要找到存活对象、复制或整理对象、更新引用,只是把可以分摊的任务并行执行。对于工作量足够大的回收,这能更充分地使用多个核心;代价是回收线程之间需要协调,回收时也会集中争用计算和内存带宽。
这条路线缓解了“一个线程处理不过来”的瓶颈,保留了暂停期间集中回收的方式。对象很多、存活数据很多时,即使所有核心一起工作,单次停顿仍可能很长。因此,提高回收吞吐与压低最长暂停,逐渐成为两种需要分别处理的目标。来源:JDK 8 Parallel GC
CMS:把老年代标记和清除放到应用运行期间
CMS 面对的问题是:老年代回收不能总让应用等待整段扫描与清理。它把主要的对象图追踪和清除工作放到并发阶段,常见的年轻代搭档仍是 ParNew,年轻代回收仍然需要暂停。
下面只保留 CMS 老年代的主要阶段,省略预清理等步骤:
初始标记 并发标记 重新标记 并发清除 STW → 应用与 GC 同时运行 → STW → 应用与 GC 同时运行 建立追踪起点 遍历对象图 补齐并发变化 回收无用空间
并发标记必须处理正在变化的引用
暂停应用时,对象图相对稳定;与应用并发时,GC 正在追踪的路径会不断变化。假设 GC 已扫描过 A,却还没有扫描 C,B 原来只能通过 C 找到。应用此时把引用转移给 A:
初始关系:根 → A 根 → C → B 应用修改:A.next = B C.next = null
如果 GC 认为 A 已经处理完,又在后来扫描 C 时看见空引用,就可能漏掉仍然被 A 使用的 B。这个教学模型说明了并发标记的核心约束:应用的修改不能让活对象从回收器的视野中消失。
CMS 通过写屏障等机制记录相关变化,在后续扫描和重新标记中补齐追踪。重新标记仍然暂停应用,完成需要收束的一致性工作。因此,CMS 缩短的是需要集中暂停的工作范围,并没有让整个老年代回收周期都变成无暂停。来源:JDK 8 CMS
不移动对象,换来了较容易并发的清除,也留下了碎片
CMS 常规老年代回收采用标记后清除,通常不会把存活对象集中搬到一起。应用与 GC 因而不必在这条常规路径上解决大范围并发搬迁,但空闲空间可能散落在存活对象之间。
空间总量足够,不代表存在一块足够大的连续空间。与此同时,已经被标记保留的对象也可能在本轮结束前失去用途,形成留待以后处理的浮动垃圾。这是并发回收过程中的保守保留,不等于这些对象永远无法回收。
应用还会在 CMS 工作期间继续分配和晋升对象。如果老年代先被用满,回收却还没完成,运行时就可能被迫进入更长的暂停处理。这暴露了 CMS 的边界:标记和清除可以并发,碎片整理与分配追赶问题却没有一起消失。
CMS 在 JDK 9 被弃用,在 JDK 14 被移除。后继工作的重点,转向了更可控的空间组织,以及进一步减少对象移动造成的停顿。来源:JDK 9 发布说明、JDK 14 发布说明
G1:把堆切成区域,再选择每次处理多少
G1 用 Region 重新组织堆。年轻代和老年代仍然存在,只是分别由一组区域组成,运行时可以改变这些区域的用途和组合。分区让回收器能够在一次回收中选择部分区域,避免总把整个老年代作为一个不可分割的工作单位。
它也继续移动存活对象,把选中区域里的活对象复制出去,然后回收整个区域。与 CMS 的常规清除方式相比,这有助于整理空间;随之而来的问题是,怎样快速找出区域外指向这些对象的引用,以及一次移动多少对象才合适。来源:JDK 8 G1
卡表和记忆集,让局部回收不必退化成全堆查找
如果一个待回收区域被其他区域引用,回收器就必须知道这些入口。否则,每收集一小块区域,都要重新检查整个堆,分区的意义就会被抵消。
G1 用卡表等结构记录哪些地址范围发生过相关引用写入,再维护区域之间的引用信息。记忆集帮助回收器定位可能来自区域外的引用位置。理解其用途比把某一版实现背成“每个 Region 永远独占一套记忆集”更有用,现代实现也可以按区域组组织相关信息。来源:现代 G1 文档
维护这些记录需要应用写屏障和后台处理参与。局部回收减少了暂停中必须查找的范围,同时也把一部分成本放到了日常引用写入和记录维护上。
SATB 保留开始标记时的可达性线索
G1 的并发标记使用 SATB,也就是 Snapshot At The Beginning。这里的“快照”是一种逻辑上的存活视图,不会在标记开始时复制一份完整的堆。
应用覆盖一个引用时,SATB 屏障可以把将被覆盖的旧引用记录下来,供标记过程继续追踪。在前面的 C 指向 B 的例子中,即使应用删除了 C.next,B 仍能通过保存下来的旧引用进入标记视野。
字段原来保存 B ↓ 应用准备覆盖这个引用 按 SATB 规则保留旧值 B 的标记线索 ↓ 字段改为新值;GC 后续仍能沿旧线索追踪 B
这个模型保守地保留本轮所需对象。某个对象即使稍后已经失去用途,也可能在本轮继续被保留,等以后回收。并发标记因此允许应用继续修改图,同时守住“不误回收活对象”的要求。
Young、Mixed 与 Full GC 处理的范围不同
G1 的年轻代收集处理年轻区域。并发标记取得老年代存活信息后,后续混合收集可以在处理年轻代的同时,带上一部分有回收价值的老年代区域。Mixed GC 因而不等于扫描、整理整个堆的 Full GC。
G1 根据回收收益、存活数据和过去观察到的处理成本,选择本次收集的区域。暂停目标最终要落实为一个工作量预算。这个预算只能尽力满足,不能理解成每次暂停都受硬性时间上限保证。
主要疏散阶段仍需要 STW:应用暂停,GC 把选中区域的存活对象复制出去,并处理相关引用。存活数据越多、引用处理越复杂,这段工作就越重。并发标记并不意味着并发搬迁,这正是理解 G1 停顿边界的关键。
G1 在 JDK 9 成为服务器级配置的默认回收器。JDK 10 又将其 Full GC 改为并行执行,缩短这条全堆处理路径的工作时间;这项改进没有把常规疏散或 Full GC 变成并发回收。来源:JDK 9 发布说明、JDK 10 发布说明
ZGC:让对象搬迁与应用执行同时进行
G1 可以控制每次疏散的规模,但只要复制存活对象仍集中发生在暂停中,停顿就会与那批工作绑定。ZGC 进一步解决对象搬迁期间的访问问题,把更多标记和重定位工作与应用并发执行。它在 JDK 11 以实验特性加入,JDK 15 成为产品特性。来源:JEP 333、JEP 377
着色引用提供状态,加载屏障负责在访问路径上协作
ZGC 在引用中携带元数据,用来判断这个引用在当前回收阶段是否需要处理。这通常被称为着色指针或着色引用。颜色不等于对象可达性的全部答案:回收器仍然需要追踪对象图、维护标记和转发信息;同一个对象的不同引用也不必在同一时刻全部处于相同状态。
应用从堆中读取对象引用时,加载屏障可以检查相关状态。能够直接使用的引用走较短路径;需要修正的引用才进入相应处理,查找有效地址,必要时参与重定位协作。下面只突出地址修正,省略复制过程中的其他并发协议:
某个引用位置里仍保存旧地址 A ↓ 加载引用及屏障检查 发现这个引用需要按当前状态处理 ↓ 查询转发信息或协助重定位 获得有效地址 B ↓ 应用继续访问有效对象 适合时修复原引用位置
这些工作由运行时插入的访问逻辑完成,业务代码不需要自己追踪对象地址。把一部分地址修正放到访问路径上,就能避免必须先暂停所有应用、统一改完全部旧引用,才能继续使用对象。
这也是屏障的代价所在:回收的一部分协调成本,被分摊到了应用执行中。不能把 ZGC 描述成“只改几位颜色,就不再需要扫描对象”,也不能把每次字段访问都理解成必然发生一次昂贵修复。机制来源:JEP 439 的 ZGC 设计说明
低停顿需要持续推进,仍然会受到 CPU 和空间约束
ZGC 仍有阶段切换等短暂停,不是完全取消 STW。实现也在持续减少暂停中的工作:JDK 16 把线程栈处理移出 GC 安全点,通过栈水位屏障让相关处理与线程执行协作完成。因此,早期版本中“暂停主要扫描全部线程栈和根”的模型,不能直接套在后续版本上。来源:JEP 376
并发回收期间,应用仍会创建对象,搬迁也需要可用空间。GC 必须在空间耗尽前推进到足以释放内存的阶段。如果回收速度追不上分配速度,申请内存的线程仍可能等待,这就是 Allocation Stall 一类现象。它与某个 GC 阶段的全局暂停不是同一个概念。
因此,短暂停不等于回收免费,也不等于任何环境下都有固定毫秒数的延迟保证。CPU 用于并发回收,额外空间用于容纳持续分配和搬迁,屏障用于维持一致性,这几项共同换取更少的集中等待。
Shenandoah:通过转发与访问屏障实现另一条并发搬迁路线
Shenandoah 同样以减少暂停为目标,在 OpenJDK 12 以实验特性加入,JDK 15 成为产品特性。它与 ZGC 并行发展,不能按出现顺序称为“ZGC 的下一代”。不同 JDK 发行版是否包含 Shenandoah,也并不一致。来源:JEP 189、JEP 379
Shenandoah 使用转发信息描述对象的有效位置,通过加载引用屏障,也就是 Load Reference Barrier,在引用交给应用使用前完成必要解析。对象已搬迁时,访问可以被引导到当前副本;某些情况下,应用线程也会协助疏散。来源:Shenandoah 加载引用屏障说明
短暂停 → 并发标记 → 短暂停 → 并发疏散 → 短暂停 → 并发更新引用 → 短暂停 找存活对象 搬迁对象 处理旧引用 教学示意:只保留主要工作,省略预处理、清理等步骤。
它不仅把标记移到并发阶段,也把疏散和引用更新放到应用运行期间。阶段转换仍需要短暂停,因此“并发标记、并发移动、并发更新”也不能翻译成“零停顿”。
后续实现还增加了引用自修复:屏障取得修正地址后,可以用比较并交换等方式更新原引用位置,减少下次访问重复解析的成本,同时避免覆盖其他线程刚写入的新值。这里的自修复修正的是运行时引用状态,业务对象并未因此发生语义上的变化。来源:Shenandoah 自修复屏障说明
两条低停顿路线都需要访问屏障,区别不能概括成“一个有读屏障,一个没有”。ZGC 利用引用中的元数据协作处理,Shenandoah 通过转发信息及其访问协议解析有效对象;具体的数据结构、阶段安排和优化方法不同。
Shenandoah 也需要 CPU 和空闲空间来支持并发推进。空间不足或回收无法按预期完成时,仍可能进入退化或完整回收路径。它减少了常规路径上的集中暂停,并没有取消内存耗尽时必须完成回收的事实。
JDK 21 之后:低停顿回收也重新利用对象寿命差异
并发搬迁缓解了暂停瓶颈,却没有自动减少需要追踪的对象总量。如果大量长寿命对象跨过许多轮回收,每次都反复处理它们,仍会消耗 CPU 和内存带宽。分代低停顿回收把寿命差异与并发搬迁结合起来:年轻对象可以更频繁地处理,老对象不必以相同频率参与。
分代 ZGC 重新分配了加载屏障与存储屏障的职责
JDK 21 引入分代 ZGC;JDK 23 将分代设为 ZGC 的默认模式;JDK 24 移除了非分代模式。这里的“默认”指启用 ZGC 后采用哪一种模式,不意味着整个 JDK 的默认回收器变成 ZGC。来源:JEP 439、JEP 474、JEP 490
分代带来一个直接需求:年轻代回收必须找到老年代指向它的引用。分代 ZGC 使用存储屏障记录相关引用字段的位置,使回收器能够从这些位置进入年轻对象图。
存储屏障还承担 SATB 标记相关工作,包括按规则保存将被覆盖的旧引用;加载屏障则主要负责去除引用中的元数据编码并处理搬迁后的地址修正。职责的重新分配很重要,早期非分代 ZGC 中加载屏障参与标记的模型,不能原封不动用于解释分代版本。
这项演进同时处理了两个层次的问题:并发搬迁继续压缩集中暂停,分代减少对长寿命对象的重复工作。代价也随之增加了跨代引用记录和相应屏障协作,不能只画出年轻代、老年代两个框就认为机制已经解释完了。
分代 Shenandoah 保留并发疏散,增加年龄信息
Shenandoah 的分代模式在 JDK 24 进入实验阶段,在 JDK 25 成为产品特性。它复用已有的加载引用屏障和并发疏散能力,并通过卡标记维护跨代引用信息。老年代并发标记可以给年轻代回收让出执行机会,后续疏散也可以带上选定的老年代区域。来源:JEP 404、JEP 521
这里也会出现“混合”处理年轻代与部分老年代的概念,但不能因此把它等同于 G1 的 Mixed GC:G1 的主要疏散在暂停中进行,Shenandoah 保留的是并发疏散路线。
截至 JDK 27,Shenandoah 的默认模式仍为单代 SATB,分代模式已经正式提供,却尚未成为默认。实验、产品特性、默认启用,是三个需要分别记住的状态。来源:JDK 27 正式版的 Shenandoah 默认模式
G1 也在继续演进:从更细的限制范围到更低的同步成本
低停顿回收器发展期间,G1 并没有停留在 JDK 9。它继续处理自身路径上的具体成本。
JDK 22 引入 Region Pinning。当本地代码在 JNI 临界区内需要固定某些对象时,G1 可以把不可移动的限制收缩到相关区域,让其他区域仍有机会进行回收。这里固定的是对象所在区域的移动能力,并不意味着被固定对象已经不需要参与正确的存活性处理。来源:JDK 22 发布说明,JEP 423
JDK 26 继续减少 G1 内部的线程同步开销,改善维护回收所需数据时对应用吞吐的影响。它优化的是既有路线中的协调成本,没有改变 G1 主要疏散阶段需要暂停这一基本事实。来源:JDK 26 发布说明,JEP 522
JDK 27 把 G1 的默认范围扩展到所有环境。对照 JDK 9 就能看清这次变化:此前是服务器级配置默认,到了 JDK 27 才扩大到全部环境。默认范围扩大与新的回收算法诞生,同样是两回事。来源:JDK 27 发布说明,JEP 523
把关键版本放回时间轴
下表只列本文主线上有实质变化的节点,没有列出的版本也可能包含实现优化,但不代表每次 JDK 发布都会更换一代回收器。
| JDK | 年份 | 垃圾回收的重要变化 |
|---|---|---|
| 8 | 2014 | Serial、Parallel、CMS、G1 已同时存在;服务器级配置通常默认 Parallel |
| 9 | 2017 | G1 成为服务器级配置默认回收器;CMS 弃用 |
| 10 | 2018 | G1 的 Full GC 改为并行执行 |
| 11 | 2018 | ZGC 以实验特性加入 |
| 12 | 2019 | Shenandoah 以实验特性加入 OpenJDK |
| 14 | 2020 | CMS 移除 |
| 15 | 2020 | ZGC、Shenandoah 成为产品特性 |
| 16 | 2021 | ZGC 并发处理线程栈,进一步减少暂停内的工作 |
| 21 | 2023 | 引入分代 ZGC |
| 22 | 2024 | G1 支持 Region Pinning |
| 23 | 2024 | 分代模式成为 ZGC 默认模式 |
| 24 | 2025 | ZGC 移除非分代模式;分代 Shenandoah 进入实验阶段 |
| 25 | 2025 | 分代 Shenandoah 成为产品特性 |
| 26 | 2026 | 减少 G1 内部同步开销 |
| 27 | 2026 | G1 默认范围扩展到所有环境;Shenandoah 默认仍为单代 SATB |
这些版本节点表达了不同性质的变化。有的是新方案第一次可以使用,有的是实现已经适合作为正式产品提供,有的只是改变默认选择。区分这三种含义,才能避免把“某年引入”和“某年成为默认”混成一个日期。
回到原理:每种回收器把成本放在哪里
回收器共同面对的工作并没有消失:找出存活对象,恢复可用空间,并保证应用看到正确的对象。演进改变了这些工作的粒度、并发方式和支付成本的时间。
| 路线 | 如何减少回收成本或暂停 | 主要仍要承担什么 |
|---|---|---|
| Serial | 用较简单的单线程流程完成分代回收 | 应用等待集中完成的扫描、复制或整理 |
| Parallel | 用多个 GC 线程加快暂停中的工作 | 仍有 STW,并集中使用核心与内存带宽 |
| CMS | 老年代标记与清除主要并发进行 | 年轻代和部分阶段暂停,碎片、浮动垃圾及分配追赶问题 |
| G1 | 分区、跨区引用记录和工作量预测控制单次范围 | 主要疏散仍暂停,平时需要维护相关记录 |
| ZGC | 通过引用元数据和屏障支持并发搬迁 | 屏障、并发 CPU、搬迁与持续分配所需空间 |
| Shenandoah | 通过转发与访问屏障并发标记、疏散、更新引用 | 屏障与转发处理、并发 CPU 和空间余量 |
分代又横跨这些路线,减少重复检查长寿命对象的工作。它与“并行还是并发”回答不同问题:前者关注哪些对象值得一起处理,后者关注这些工作由谁完成、应用是否需要等待。
从 JDK 8 走到今天,GC 的重要进步在于:越来越多工作可以按对象寿命和区域拆分,越来越少工作必须让所有应用线程一起等着完成。与此同时,成本仍然存在于 CPU、引用访问、辅助记录和空闲空间中。理解了成本的去向,才能真正理解每一种回收器解决了什么。


