5364 words
27 minutes
JVM 垃圾收集器演进与核心概念

一、 引言:为什么我们需要了解 GC?#

什么是垃圾收集?#

垃圾收集(Garbage Collection, 简称 GC)是 JVM 自动管理内存的核心机制。在早期的编程语言(如 C/C++)中,开发者需要手动分配和释放内存,这极易导致内存泄漏或悬空指针。Java 通过 GC 机制,由虚拟机自动识别并回收不再使用的对象,将程序员从繁重的内存管理中解放出来,专注于业务逻辑的实现。

评价 GC 的核心指标#

衡量一个垃圾收集器的性能,通常从以下三个维度进行考量,这三者也被称为“不可能三角”,往往需要根据业务场景进行取舍:

  • 吞吐量 (Throughput):运行用户代码的时间占总运行时间的比例。计算公式为:。高吞吐量意味着在单位时间内完成更多的任务,适合后台计算任务。
  • 暂停时间 (Pause Time/STW):指在垃圾收集过程中,由于“Stop The World”机制导致用户线程停顿的时间。对于响应速度敏感的应用(如网页服务、游戏),低延迟是核心追求。
  • 内存占用 (Footprint):垃圾收集器运行所需要的额外内存开销。随着硬件成本降低,这一指标的优先级在现代应用中有所下降,但在嵌入式或微服务场景下依然重要。

二、 JVM 内存模型与分代假说#

弱分代假说与强分代假说#

现代 JVM 垃圾收集器的设计并非凭空想象,而是基于大量实际应用数据的观察得出的“分代假说”:

  1. 弱分代假说 (Weak Generational Hypothesis):绝大多数对象都是“朝生夕灭”的,在创建后很快就会变得不可达。
  2. 强分代假说 (Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡,应当将其移入更稳定的区域。

基于这些假说,JVM 将堆内存划分为不同的区域,对不同生命周期的对象采用最高效的算法进行处理。

堆内存结构:Eden, Survivor, Old Generation#

典型的 JVM 堆内存(以分代模型为例)分为 新生代 (Young Generation)老年代 (Old Generation)

  • 新生代

  • Eden 区:大部分新对象在此分配。

  • Survivor 区 (S0/S1):存放经历过 Minor GC 后依然存活的对象。S0 和 S1 轮换使用,确保内存碎片的整理。

  • 老年代:存放生命周期较长或大对象。当对象在 Survivor 区经历足够多次回收后,会晋升到老年代。

三、 JVM 垃圾收集器的发展历程#

JVM 收集器的演进过程是一部不断挑战“停顿时间”极限的历史:

  1. 单线程阶段 (Serial GC):早期的 Java 版本使用单线程进行回收,虽然简单可靠,但在多核时代效率极低。
  2. 多线程并行阶段 (Parallel GC):随着多核 CPU 的普及,出现了并行收集器,利用多个 GC 线程共同工作,极大提升了吞吐量。
  3. 并发标记阶段 (CMS):为了解决 STW 时间过长的问题,CMS 尝试让垃圾回收线程与用户线程“并发”执行,标志着低延迟时代的开启。
  4. 地域化 (Region) 阶段 (G1/ZGC):现代收集器打破了物理上连续的分代限制。G1 引入了 Region 的概念,而 ZGC 进一步通过染色指针等技术,实现了在超大内存下依然能保持极低停顿的跨越式进步。

四、 核心收集器深度解析#

1. Serial / Serial Old 收集器#

原理 Serial 收集器是 JVM 最基础、历史最悠久的收集器。正如其名,“Serial” 意味着它是单线程的。在进行垃圾收集时,它不仅只使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是,在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。

流程 其执行流程遵循经典的“标记-复制”(新生代)或“标记-整理”(老年代)算法:

  1. 触发 GC:当新生代或老年代空间不足时发起。
  2. Stop The World (STW):立即暂停所有正在运行的用户线程。
  3. 单线程回收:由单条垃圾收集线程独立完成对象标记、移动和清理工作。
  4. 恢复运行:收集完成后,恢复用户线程的执行。

特点

  • 简单而高效:它是所有收集器中额外内存消耗(Memory Footprint)最小的,且由于没有线程交互的开销,在单线程环境下,其运行效率极高。
  • 停顿感明显:对于现代多核 CPU 来说,单线程处理大数据量会产生较长的 STW 时间,导致应用出现明显的“卡顿”现象。

适用场景

  • Client 模式:通常作为 HotSpot 虚拟机在 Client 模式下的默认新生代收集器。
  • 单核微服务:在云原生环境下,如果分配给容器的资源仅为 1 核 CPU,Serial 收集器没有线程切换开销的优势反而能得到发挥。

2. Parallel Scavenge / Parallel Old 收集器#

原理 Parallel Scavenge 收集器也是一款针对新生代的收集器,同样基于“标记-复制”算法实现。与 Serial 收集器不同,它是多线程并行的。Parallel Old 则是其配套的老年代版本,使用多线程和“标记-整理”算法。它的设计目标与 CMS 等收集器关注停顿时间不同,它更关注吞吐量(Throughput),因此常被称为“吞吐量优先”收集器。

流程

  1. Stop The World (STW):当触发 GC 时,首先暂停所有用户线程。
  2. 多线程并行回收:与 Serial 收集器单线程工作不同,Parallel 收集器会开启多条 GC 线程,利用多核 CPU 的优势,并行执行标记和清理工作。
  3. 恢复运行:回收任务完成后,用户线程继续执行。

特点

  • 高吞吐量:能够高效地利用 CPU 时间,尽快完成程序的运算任务。
  • 自适应调节策略:这是它区别于其他收集器的重要特征。通过开启 -XX:+UseAdaptiveSizePolicy,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整 Eden 与 Survivor 区的比例、晋升老年代对象年龄等参数,以提供最合适的停顿时间或最大的吞吐量。
  • 控制停顿时间:允许通过 -XX:MaxGCPauseMillis 设置最大垃圾收集停顿时间,但缩短停顿时间往往是以牺牲吞吐量和新生代空间为代价的。

适用场景

  • 后台任务:适用于对响应时间要求不高,但需要高效率利用 CPU 的场景,如大批量数据处理、科学计算、订单处理等后台服务。
  • 多核服务器:默认是 JDK 8 中的默认收集器,非常适合在多处理器环境下运行。

3. CMS (Concurrent Mark Sweep) 收集器#

原理 CMS 收集器是 JVM 历史上第一款真正意义上的并发(Concurrent)收集器。它的设计目标是获取最短回收停顿时间,彻底改变了以往“垃圾回收时必须全程停止用户程序”的局面。它基于 “标记-清除”(Mark-Sweep) 算法,主要应用于老年代的回收。

流程:深度解析四阶段 CMS 的运作过程比以往的收集器更为复杂,为了实现并发,它将回收过程拆解为四个核心阶段:

  1. 初始标记 (Initial Mark) [STW]:仅标记 GC Roots 能直接关联到的对象,以及被新生代对象引用的老年代对象。虽然会触发 STW,但由于路径极短,速度非常快。
  2. 并发标记 (Concurrent Mark):从“初始标记”的对象开始,遍历整个老年代的对象图进行可达性分析。耗时最长,但与用户线程并发执行,用户感知不到卡顿。
  3. 重新标记 (Remark) [STW]:修正并发标记期间,因用户程序继续运作而导致引用关系产生变动的那一部分记录。停顿时间虽比初始标记长,但远短于并发标记。
  4. 并发清除 (Concurrent Sweep):清理并删除标记阶段判断为已经死亡的对象。由于不需要移动存活对象,该阶段可以并发执行。

底层机制:如何解决并发冲突? 在“并发标记”期间,对象间的引用关系可能随时发生变化,CMS 引入了以下核心技术来保证回收的准确性:

  • 三色标记算法(Tri-color Marking)

  • 白色:尚未访问的对象(回收目标)。

  • 灰色:已访问但引用的子对象尚未扫描完。

  • 黑色:已访问且引用的所有子对象都已扫描完。

  • 增量更新(Incremental Update):为了防止黑色对象重新指向白色对象导致“漏标”,CMS 通过**写屏障(Write Barrier)**技术,将这种新发生的引用记录下来。在“重新标记”阶段,以这些黑色对象为根再次扫描,确保不误杀存活对象。

  • 卡表(Card Table):老年代被划分为一个个“卡页”。若某页中对象的引用发生了变化,该页会被标记为 “Dirty”。GC 时只需扫描这些 Dirty 卡页,而无需扫描整个老年代或新生代,极大提升了效率。

核心痛点(CMS 的致命伤) 虽然 CMS 开启了并发时代,但它也存在三个显著的缺陷,最终导致它在 JDK 14 中被彻底移除:

  • 空间碎片化:基于“标记-清除”算法,回收后不整理内存,导致大量碎片。当大对象无法找到连续空间时,会频繁触发 Full GC。
  • Concurrent Mode Failure:如果在并发清除阶段,用户线程产生的**浮动垃圾(Floating Garbage)**过快,导致预留的老年代空间不足,JVM 将不得不启动“备用方案”——冻结所有线程,改用单线程的 Serial Old 收集器进行全堆回收,产生极长的停顿。
  • 抢占 CPU 资源:并发阶段会占用一部分核心资源,在 CPU 核心较少的机器上,会导致应用程序响应明显变慢。

4. G1 (Garbage First) 收集器#

原理 G1 是 JVM 垃圾收集器史上的一个巨大飞跃。它彻底抛弃了物理上的固定分代界限,不再要求 Eden、Survivor 和 Old 区域在内存上必须是连续的。G1 将整个堆划分为约 2048 个大小相等的独立区域(Region)。每个 Region 都可以根据需要扮演 Eden、Survivor 或 Old 角色,此外还增加了一个专门存放巨型对象(大于 Region 容量 50%)的 Humongous 区域。

运作流程 G1 的回收过程分为四个主要阶段:

  1. 初始标记 (Initial Mark) [STW]:标记 GC Roots 直接关联的对象,并修改 TAMS(Next Top at Mark Start)指针。此阶段借用 Minor GC 的停顿完成,耗时极短。
  2. 并发标记 (Concurrent Marking):从 GC Roots 开始对堆中对象进行可达性分析。此阶段与用户线程并发执行,耗时较长。
  3. 最终标记 (Final Mark) [STW]:处理并发标记阶段产生的遗留变化。
  4. 筛选回收 (Live Data Counting and Evacuation) [STW]:计算各 Region 的回收价值和成本,根据用户设定的 -XX:MaxGCPauseMillis(默认 200ms)来挑选性价比最高的 Region 组成“回收集”(Collection Set),将存活对象复制到空闲 Region 中。

底层机制:跨 Region 引用的处理 在分代模型中,老年代引用新生代很常见。在 G1 的 Region 结构中,跨 Region 引用更加频繁。G1 引入了以下核心技术来保证回收效率:

  • 记忆集 (Remembered Set, RSet): 每一个 Region 都有一个对应的 RSet。它记录了“谁引用了我”。在进行垃圾回收时,G1 只需要扫描当前 Region 的 RSet,就能找到所有指向该 Region 的外部引用,从而避免了全堆扫描。
  • SATB(Snapshot-At-The-Beginning,原始快照): 这是 G1 在并发阶段解决“漏标”问题的算法,与 CMS 的增量更新不同:
  • 在并发标记开始时,G1 对对象图做一份“快照”。
  • 写前屏障 (Pre-Write Barrier):当用户线程准备修改一个引用时,G1 会记录下该引用原本指向的对象。
  • 逻辑:即便该对象在并发期间被切断了引用,G1 依然认为它是存活的(属于快照的一部分)。虽然这会产生一些“浮动垃圾”,但它极大提高了重新标记阶段的效率,避免了类似 CMS 的深度重新扫描。

特点

  • 可预测的停顿:这是 G1 最大的亮点。它能让开发人员明确指定“在 M 毫秒的时间片内,消耗在 GC 上的时间不得超过 N 毫秒”。
  • 内存整理:由于回收过程是基于 Region 之间的“复制”算法,回收后的内存是完全规整的,彻底解决了 CMS 严重的内存碎片问题。
  • 分而治之:优先回收垃圾最多、回收收益最大的区域(故名 Garbage First)。

适用场景

  • 服务端大内存应用:通常建议在堆内存在 6GB 到 8GB 以上的应用中使用 G1。
  • 追求停顿时间可控:需要替代 CMS 且对延迟有一定要求的系统。

非常抱歉,是我理解偏差。现在为您重写 ZGC (Z Garbage Collector) 的深度技术解析,确保涵盖所有底层核心细节,不作过度精简。


ZGC (Z Garbage Collector) 收集器#

ZGC 是一款基于 Region 内存布局,不设分代(JDK 21 之前),并使用了染色指针读屏障技术来实现可伸缩低延迟的垃圾回收器。其核心设计目标是:无论堆内存多大(支持从 8MB 到 16TB),都能将垃圾回收引起的停顿时间(STW)控制在 10 毫秒以内。

1. 核心运作流程#

ZGC 的绝大部分工作(包括最耗时的对象转移)都是并发执行的,其回收周期主要分为以下四个阶段:

  • 初始标记 (Pause Mark Start) [STW]:从 GC Roots 开始标记直接关联的对象。由于只扫描根对象,停顿时间极短且固定,不随堆大小增长。
  • 并发标记 (Concurrent Mark):遍历整个对象图进行可达性分析。此阶段与用户线程并发执行。
  • 并发转移准备 (Concurrent Prepare for Relocate):分析哪些 Region 需要清理,并将需要清理的 Region 组成回收集(Relocation Set)。
  • 并发转移 (Concurrent Relocate):这是 ZGC 的核心,将存活对象从旧 Region 复制到新 Region。通过“染色指针”和“读屏障”技术,确保了在对象移动过程中,用户线程依然可以无感知地并发访问。

2. 核心技术:染色指针 (Colored Pointers)#

在传统收集器中,对象状态存放在对象头中。ZGC 另辟蹊径,将标记信息直接存储在 64 位指针 本身。

  • 位分布:在 64 位系统中,由于处理器寻址限制,ZGC 利用了指针中未使用的第 42 到 45 位来存储 4 个标志位:

  • Marked0 / Marked1:用于标记对象在当前周期内是否存活。

  • Remapped:表示指针是否已经指向了对象移动后的新地址。

  • Finalizable:表示对象是否只能通过 finalize() 方法访问。

  • 优势:这种设计使得 JVM 只要获取了指针,无需访问内存中的对象头(减少一次内存寻址),就能瞬间得知对象的 GC 状态,大幅提升了标记阶段的效率。

3. 多重映射 (Multi-Mapping)#

由于指针中包含了标志位,为了让底层的操作系统和处理器能正常识别这些“带标记”的地址,ZGC 采用了多重映射技术:

  • 它将同一块物理内存同时映射到三个不同的虚拟地址视图:Marked0 视图Marked1 视图Remapped 视图
  • 这意味着,无论指针的标志位如何变化,底层硬件最终都能定位到同一块物理内存。这使得染色指针在硬件层面能像普通指针一样正常工作。

4. 读屏障 (Read Barrier) 与 对象“自愈” (Self-Healing)#

在“并发转移”阶段,对象可能正在被移动。如果用户线程尝试读取一个位于回收集中的对象,ZGC 通过读屏障介入处理:

  • 触发条件:当用户线程从堆中加载一个对象引用(即读取指针)时。
  • 自愈逻辑
  1. 读屏障会检查该指针的 Remapped 位。
  2. 如果发现对象已被移动(Remapped 为 0),读屏障会查找该对象内部维护的 转发表 (Forwarding Table)
  3. 读屏障会获取新地址,并立即更新当前引用的指针指向新地址,同时修正 Remapped 标志。
  • 性能保障:这种“自愈”机制确保了只有第一次访问旧指针时会有微小的查表损耗,后续访问将直接定位到新地址,极大降低了并发回收对应用性能的影响。

5. 动态 Region 布局#

与 G1 的固定大小 Region 不同,ZGC 的 Region 是动态生成的,以应对不同大小的对象:

  • Small Region (2MB):存放 256KB 以下的小对象。
  • Medium Region (32MB):存放 256KB 到 4MB 之间的对象。
  • Large Region (N × 2MB):容量动态变化,专门存放大于 4MB 的巨型对象。每个 Large Region 只存放一个对象,虽然称为“大”,但其实际占用空间可能仅比 Medium 大一点。

6. NUMA 感知 (NUMA-Aware)#

ZGC 能够感知 NUMA (Non-Uniform Memory Access) 架构。它会优先在当前执行线程所属 CPU 核心的本地内存上分配对象。在多 CPU 插槽的服务器上,这种设计能显著降低跨核内存访问的延迟,使系统的整体吞吐量在多核环境下表现更优。

五、 如何选择合适的收集器?#

在众多的收集器中,没有绝对的“最强者”,只有最适合业务场景的“最优解”。选择垃圾收集器本质上是在吞吐量延迟资源消耗之间进行权衡。

以下是一份基于实战经验的决策路径:

决策树指南#

  • 内存资源极度受限(< 100MB)?

  • 推荐:Serial GC (-XX:+UseSerialGC)

  • 理由:在这种规模下,多线程的交互开销反而会拖慢速度。Serial GC 内存占用最小,简单且高效。

  • 追求最高吞吐量,且能忍受较长的后台停顿?

  • 推荐:Parallel GC (-XX:+UseParallelGC)

  • 理由:这是“吞吐量优先”的选择,适合科学计算、大规模数据批处理等不涉及实时交互的后台任务。

  • 追求响应速度,内存处于中大型规模(4-6GB 以上)?

  • 推荐:G1 GC (-XX:+UseG1GC)

  • 理由:G1 是目前最主流的通用收集器,它提供了可预测的停顿时间模型。如果你希望平衡吞吐量和延迟,且内存已步入 GB 级别,G1 是默认的首选。

  • 追求极致低延迟,且拥有 TB 级别的超大堆内存?

  • 推荐:ZGC (-XX:+UseZGC)

  • 理由:如果业务无法容忍超过 10ms 的停顿,或者堆内存巨大(如分布式缓存、实时风控系统),ZGC 的染色指针和读屏障技术能提供亚毫秒级的延迟体验。

六、 结语:GC 的未来趋势#

随着硬件性能的提升和云计算架构的普及,JVM 垃圾收集技术正在向着“极致性能”和“零调优”两个方向进化。

1. 云原生环境下的 Epsilon (No-op) 收集器#

JDK 11 引入了 Epsilon 收集器,这是一款不执行任何实际垃圾回收行为的“无为而治”收集器。

  • 核心逻辑:它只负责内存分配,不负责内存回收。
  • 应用价值:在云原生微服务中,如果容器的生命周期极短(运行完即销毁),或者为了追求性能测试的基准数据,Epsilon 可以完全消除 GC 带来的性能波动。

2. 更加智能化的自动调优(Self-tuning)#

未来的 GC 演进将逐渐减少人工干预。

  • AI 驱动:通过机器学习分析运行时的内存变化模式,动态调整 Region 大小、触发阈值和停顿时间目标。
  • 自动化平衡:如 JDK 21 进一步增强了分代 ZGC(Generational ZGC),在保持超低延迟的同时,通过自动识别对象年龄分布,显著降低了原本非分代 ZGC 带来的 CPU 吞吐量损耗。

JVM 垃圾收集器的历史,就是一部人类与内存碎片、系统停顿作斗争的历史。从手动管理到全自动化的毫秒级回收,GC 的进步让开发者能够将更多精力投入到改变世界的业务逻辑之中。

JVM 垃圾收集器演进与核心概念
https://mj3622.github.io/posts/学习笔记/java/垃圾收集器/
Author
Minjer
Published at
2026-02-11