第一部分:垃圾回收机制——自动内存管理的核心原理

1.1 核心问题:内存为何需要管理?

在程序运行时,创建对象(如变量、数组、结构体实例)会向操作系统申请内存。如果程序持续运行并不断创建新对象,而旧对象不再使用后其内存未被释放,可用的内存就会逐渐耗尽,最终导致程序因内存不足而崩溃。这种现象被称为“内存泄漏”。

手动内存管理(如在C/C++中调用freedelete)要求开发者精确追踪每一块内存的生命周期,这极易出错。垃圾回收(Garbage Collection, GC)机制的目标就是将此过程自动化。

1.2 定义:什么是垃圾?

在垃圾回收的语境中,“垃圾”指那些程序已经无法再访问,但仍旧占据着内存空间的对象。判断依据不是开发者主观上“觉得”它没用了,而是通过客观的、形式化的“可达性”分析来确定的。

1.3 技术基石:可达性分析

这是现代垃圾回收器判定对象生死的根本算法。其逻辑非常直接:

  1. 定义一组“根”(GC Roots):这是所有对象引用链的绝对起点。通常包括:

    • 当前执行方法中局部变量表里的引用(栈帧中的局部变量)。
    • 活跃线程的栈上的引用。
    • 类的静态属性(在方法区)的引用。
    • 常量池中的引用等。
  2. 从“根”开始遍历:垃圾回收器会将所有“GC Roots”作为起点,开始遍历整个内存中的对象引用关系图。它顺着根对象找到它们直接引用的对象A,再从A找到A引用的对象B,如此层层递进。

  3. 标记可达对象:所有能被这个遍历过程访问到的对象,都被标记为“可达”(Alive)。这意味着从程序的任何活跃上下文出发,最终都能“摸到”这个对象。

  4. 判定垃圾:遍历结束后,所有未被标记的对象,就意味着从任何“根”出发都无法抵达它们。程序已经彻底失去了访问它们的途径。这些对象就是“不可达”的,即垃圾。

一个简单的比喻(尽管要求少用,但此处为最直白的解释):假设“GC Roots”是你本人手上正拿着的几个气球。气球下面可能用绳子连着其他气球(引用关系)。所有你能直接或间接通过绳子拽到的气球,都是“可达”的。而某个气球绳子断了,飘到了你完全看不到也够不着的地方,它就是“不可达”的垃圾。

1.4 基础工作流程:标记-清除

基于可达性分析,最基础的垃圾回收算法“标记-清除”(Mark-Sweep)按两步工作:

  • 标记阶段(Mark):暂停所有应用线程(这个暂停称为“Stop-The-World”),从GC Roots开始进行上述的遍历和标记。这个阶段只做标记,不回收。
  • 清除阶段(Sweep):再次遍历堆内存。这次,所有未被标记为“可达”的内存块,都会被回收器记录为空闲区域。这些区域的内存被释放,可供后续分配新对象使用。这个过程通常不移动存活对象。

存在的问题

  • 内存碎片:清除后,可用内存和已用内存会交错在一起,形成碎片。后续可能难以分配大对象。
  • 暂停时间:在标记阶段必须暂停应用,如果堆内存很大,遍历标记耗时就会长,导致应用卡顿。

第二部分:分代回收假说与内存区域划分

2.1 核心观察:对象寿命的“二八定律”

在对大量实际应用程序的运行时行为进行分析后,发现了一个关键模式,即 “弱分代假说” :绝大多数对象的寿命都非常短暂,它们在分配后很快就不再被使用。

基于这一观察,垃圾回收器可以做出一个重要的性能优化假设:将堆内存按照对象的预期寿命进行物理或逻辑上的划分,并对不同区域采用不同的回收策略。 这样可以将有限的回收资源集中在最可能产生垃圾的区域,从而大幅提升效率。

2.2 堆内存的分代划分

现代垃圾回收器(如 HotSpot JVM、.NET CLR 的托管堆)普遍将堆划分为两个主要区域:

1. 年轻代 (Young Generation)

  • 目的:专门用于分配新创建的对象。
  • 设计依据:因为大多数对象朝生夕死,所以这个区域会非常频繁地进行垃圾回收。
  • 内部结构:年轻代通常被进一步划分为三个部分:
    • 一个 Eden 区(伊甸园区):所有新对象首先在这里分配。
    • 两个 Survivor 区(幸存者区,通常称为 S0 和 S1):用于存放经历了一次年轻代垃圾回收后仍然存活的对象。

2. 老年代 (Old Generation / Tenured Generation)

  • 目的:存放寿命较长的对象。
  • 晋升机制:在年轻代中,如果一个对象在经历了多次(具体次数可配置,称为“晋升阈值”)垃圾回收后仍然存活,垃圾回收器会认为它是一个有长期存活潜力的对象,并将其从年轻代移动到老年代。这个过程称为 “晋升”“老化”
  • 设计依据:老年代的对象存活率高,因此针对这个区域的垃圾回收频率远低于年轻代,但每次回收可能耗时更长,因为需要检查和移动的对象更多。

(在某些实现中,还存在一个 永久代 (Permanent Generation) 或元空间 (Metaspace),主要用于存储类的元数据等信息,其回收行为与对象堆不同,这里暂不展开。)

2.3 分代收集的工作流程

一次完整的、基于分代的对象分配与回收周期通常如下:

  1. 对象分配

    • 几乎所有新对象都在 Eden 区 分配。
    • 当 Eden 区内存被填满时,会触发一次针对年轻代的垃圾回收,称为 Minor GCYoung GC
  2. 年轻代回收(Minor GC)

    • 暂停应用线程,进行可达性分析。注意,此时 GC Roots 不仅包括全局根(如静态变量),还包括老年代对象对年轻代对象的引用。
    • 标记出 Eden 区和 一个 Survivor 区(比如 S0)中所有存活的对象。
    • 将这些存活的对象 一次性复制到另一个空的 Survivor 区(比如 S1)。这个“复制”过程天然地完成了内存的整理,使得 S1 区的一端是紧密排列的存活对象,另一端是完整的空闲内存。
    • 直接清空 Eden 区和刚使用过的 Survivor 区(S0)。注意,清除是通过复制存活对象、直接覆盖原区域来实现的,速度极快
    • 此时,S1 区成为了下一次回收时的“来源”区(From Survivor)。
    • 对象在 Survivor 区之间每经历一次 Minor GC 且存活,其“年龄”就增加 1。当年龄超过阈值(例如 15),在下一次 Minor GC 时,它就会被晋升到老年代。
  3. 老年代回收(Major GC / Full GC)

    • 当老年代的内存空间也被填满时,会触发针对整个堆的垃圾回收,称为 Major GCFull GC
    • Full GC 会同时回收年轻代和老年代,通常使用与年轻代不同的算法(例如“标记-清除-整理”)。
    • 由于需要处理整个堆,且老年代存活对象多,Full GC 的 暂停时间通常比 Minor GC 长得多,对应用性能的影响显著。因此,GC 调优的一个重要目标就是减少 Full GC 的发生频率。

2.4 分代设计的优势

  1. 高效利用局部性:高频的 Minor GC 只扫描和复制年轻代,而年轻代通常只占整个堆的较小部分(例如 1/3),因此回收速度很快,应用暂停时间短。
  2. 针对性算法:对死亡率高的年轻代,采用高效的 “复制”算法,用空间换时间,且无内存碎片。对存活率高的老年代,采用更节省空间的 “标记-清除”或“标记-整理”算法
  3. 吞吐量提升:将回收工作集中在产出垃圾最多的区域,使得单位时间内回收的内存效率最高,整体应用吞吐量得到提升。

第三部分:主流垃圾回收器演进 —— CMS、G1 与 ZGC

现代垃圾回收器的演进史,本质上是工程师们为解决一个核心矛盾而奋斗的历史:如何在不牺牲吞吐量的前提下,尽可能地减少垃圾回收带来的应用暂停时间(Stop-The-World, STW)。从 CMS 到 G1,再到 ZGC,每一代都代表了不同的设计哲学和权衡艺术。

3.1 并发标记清除回收器

设计目标与定位
CMS 是历史上第一个真正意义上追求 低延迟 的垃圾回收器。它的核心目标是减少老年代回收带来的长暂停,通过与应用程序线程并发工作来实现。

工作原理(四阶段)
CMS 的回收过程不再是简单的一次性 “标记-清除”,而是分为四个主要阶段,其中两个阶段是与应用线程 并发 进行的:

  1. 初始标记:STW 阶段。但速度极快,仅标记从 GC Roots 直接可达 的老年代对象。
  2. 并发标记并发阶段。从初始标记的对象出发,遍历整个老年代对象图,标记所有可达对象。此阶段与应用线程一起运行。
  3. 重新标记:STW 阶段。用于修正 并发标记 阶段中,因应用线程继续运行而导致引用关系发生变化的那部分对象。该阶段使用增量更新算法,速度比初始标记慢,但远快于扫描整个老年代。
  4. 并发清除并发阶段。回收那些在标记阶段被判定为不可达的对象所占用的内存空间。

核心优势与代价

  • 优势:将最耗时的遍历和清除工作与应用程序并发执行,显著降低了老年代回收的 STW 停顿时间。
  • 代价
    • 吞吐量降低:并发执行会占用一部分CPU资源,从而挤占应用线程。
    • 浮动垃圾:并发清除阶段,应用线程可能继续产生垃圾,这些垃圾只能等到下次回收。
    • 内存碎片:使用“标记-清除”算法,不整理内存,长期运行后可能因碎片过多触发一次耗时的 Full GC。
    • 对CPU资源敏感:在CPU核心数少的情况下,并发争抢会严重影响性能。

CMS 标志着 GC 设计从单纯追求吞吐量转向关注延迟,但其架构的固有缺陷使其最终被更先进的回收器取代。

3.2 垃圾优先回收器

设计理念的革新:化整为零
G1 放弃了传统的、物理上连续固定的年轻代和老年代划分。它将整个堆内存划分为多个大小相等(如1MB至32MB)的 区域。每个区域在逻辑上被动态地赋予角色:Eden区、Survivor区、Old区,还有一个特殊的 大对象区域

核心工作流程:收集集合
G1 不再进行全堆扫描或整个老年代的回收。它的回收过程称为 “混合回收” ,其核心是 回收价值最大化

  1. 后台并发标记:类似于CMS,进行全局并发标记,以识别出哪些区域是“完全空闲的”,哪些区域是“包含大量垃圾的”。
  2. 筛选回收:G1 会计算每个区域的“回收价值”(即回收该区域能获得多少空间、需要多少时间),并优先选择 垃圾比例最高、回收收益最大 的若干个区域组成 “收集集合”
  3. 转移存活对象:STW 阶段。将 CSet 中所有存活的对象 复制 到其他空闲区域,然后直接清空整个 CSet 区域。这个过程实现了 “标记-复制” 的效果,天然地整理了内存,避免了碎片。

G1 的关键进步

  • 可预测的停顿模型:允许用户设置期望的最大暂停时间目标,G1 会通过调整每次回收的 CSet 大小(即回收区域的数量)来尽力满足。
  • 整体效率更高:既在年轻代(由多个 Eden 和 Survivor 区域组成)使用高效的复制算法,又能以部分区域为粒度管理老年代,兼顾了吞吐量和延迟。
  • 有效控制碎片:通过跨区域的复制整理,有效避免了内存碎片问题。

G1 成为了一个在吞吐量和延迟之间取得更好平衡的 通用型 回收器,是 JDK 9 及以后的默认回收器。

3.3 Z垃圾回收器

设计目标:突破延迟极限
ZGC 是为了应对大数据、实时分析等场景下,对 超大堆内存(TB级别)超低停顿(10毫秒以内) 的极端需求而设计的。它的目标是几乎完全消除垃圾回收停顿对应用响应时间的影响。

核心技术:染色指针与读屏障
ZGC 实现革命性突破的关键在于两项技术创新:

  1. 染色指针:ZGC 将对象的元数据信息(如标记状态、转发地址)存储在对象指针本身的最高几位中,而不是存储在对象头里。这意味着一个对象在内存中的“位置”不仅包含地址,还包含了GC需要的状态信息。
  2. 读屏障:这是由 JVM 在对象访问时自动插入的一小段代码。当应用线程从堆中读取一个对象的引用时,读屏障会被触发。它的核心作用是 “解引用”:检查染色指针的状态,如果发现该对象正在被转移,读屏障会确保线程拿到的是转移后的新地址,并可能“帮忙”完成转移工作。正是通过读屏障,ZGC 得以将耗时的对象转移阶段与应用线程并发执行。

工作阶段
ZGC 的回收周期同样是并发的,主要分为:

  1. 并发标记:遍历对象图,标记存活对象。
  2. 并发预备重分配:确定本次要清理哪些区域。
  3. 并发重分配核心阶段。将存活对象复制到新区域。这是最耗时的部分,但得益于染色指针和读屏障,完全与应用程序并发进行。
  4. 并发重映射:修正所有指向旧对象地址的指针,使其指向新地址。

ZGC 的优势与现状

  • 优势:实现了 亚毫秒级到十毫秒级 的 STW 停顿,且停顿时间不随堆大小增长而增加。支持 TB 级堆内存。
  • 现状:在 JDK 15 中成为生产可用特性。它主要牺牲了一定的吞吐量(读屏障开销)来换取极致的低延迟,适用于对延迟极度敏感的应用。

演进总结

从 CMS 到 G1 再到 ZGC,我们看到一条清晰的技术演进路径:

  • CMS“我要并发” —— 开创了并发回收的道路,但留下了碎片和复杂度的问题。
  • G1“我要可预测” —— 通过区域化分代和基于价值的回收,提供了更好的综合性能和可预测的停顿。
  • ZGC“我要极致” —— 利用染色指针和读屏障等全新硬件/软件协同设计,将延迟目标推向了物理极限。

选择哪种回收器,取决于应用的核心诉求:是高吞吐、可预测的短暂停顿,还是极致的低延迟。理解它们的原理,是做出正确选择的第一步。

总结:垃圾回收器的演进与权衡的艺术

回顾我们对垃圾回收机制的探讨,可以看到一条清晰的技术演进路径,其核心驱动力始终围绕着计算机科学中的一个经典三角难题:在内存(空间)、吞吐量(计算资源)和延迟(响应时间)之间,如何取得最符合场景需求的平衡

下表总结了三大主流回收器的核心权衡:

特性 CMS (并发标记清除) G1 (垃圾优先) ZGC (Z垃圾回收器)
主要目标 降低老年代回收的延迟 在可预测的停顿下,兼顾吞吐与延迟 极致低延迟,支持超大堆
堆布局 物理连续的年轻代/老年代 分区的堆 (Region) 分区的堆 (Region)
核心算法 并发标记-清除 标记-复制 (以Region为单位) 并发标记-复制
停顿时间 中等,初始标记与重新标记阶段STW 可预测,通过调整CSet大小控制 极短且固定 (<10ms),与堆大小无关
内存碎片 严重(标记-清除算法) 很少(复制算法整理) 无(并发复制整理)
吞吐量影响 明显(并发争抢CPU) 中等 较高(读屏障开销)
适用场景 对延迟敏感、CPU资源充足的中小规模应用 JDK9+默认,通用服务器应用,需平衡吞吐与延迟 超大规模内存、对延迟极端敏感的应用(如实时分析、金融交易)
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐