【Java】JVM的垃圾回收器
第一部分:垃圾回收机制——自动内存管理的核心原理
1.1 核心问题:内存为何需要管理?
在程序运行时,创建对象(如变量、数组、结构体实例)会向操作系统申请内存。如果程序持续运行并不断创建新对象,而旧对象不再使用后其内存未被释放,可用的内存就会逐渐耗尽,最终导致程序因内存不足而崩溃。这种现象被称为“内存泄漏”。
手动内存管理(如在C/C++中调用free或delete)要求开发者精确追踪每一块内存的生命周期,这极易出错。垃圾回收(Garbage Collection, GC)机制的目标就是将此过程自动化。
1.2 定义:什么是垃圾?
在垃圾回收的语境中,“垃圾”指那些程序已经无法再访问,但仍旧占据着内存空间的对象。判断依据不是开发者主观上“觉得”它没用了,而是通过客观的、形式化的“可达性”分析来确定的。
1.3 技术基石:可达性分析
这是现代垃圾回收器判定对象生死的根本算法。其逻辑非常直接:
-
定义一组“根”(GC Roots):这是所有对象引用链的绝对起点。通常包括:
- 当前执行方法中局部变量表里的引用(栈帧中的局部变量)。
- 活跃线程的栈上的引用。
- 类的静态属性(在方法区)的引用。
- 常量池中的引用等。
-
从“根”开始遍历:垃圾回收器会将所有“GC Roots”作为起点,开始遍历整个内存中的对象引用关系图。它顺着根对象找到它们直接引用的对象A,再从A找到A引用的对象B,如此层层递进。
-
标记可达对象:所有能被这个遍历过程访问到的对象,都被标记为“可达”(Alive)。这意味着从程序的任何活跃上下文出发,最终都能“摸到”这个对象。
-
判定垃圾:遍历结束后,所有未被标记的对象,就意味着从任何“根”出发都无法抵达它们。程序已经彻底失去了访问它们的途径。这些对象就是“不可达”的,即垃圾。
一个简单的比喻(尽管要求少用,但此处为最直白的解释):假设“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 分代收集的工作流程
一次完整的、基于分代的对象分配与回收周期通常如下:
-
对象分配:
- 几乎所有新对象都在 Eden 区 分配。
- 当 Eden 区内存被填满时,会触发一次针对年轻代的垃圾回收,称为 Minor GC 或 Young GC。
-
年轻代回收(Minor GC):
- 暂停应用线程,进行可达性分析。注意,此时 GC Roots 不仅包括全局根(如静态变量),还包括老年代对象对年轻代对象的引用。
- 标记出 Eden 区和 一个 Survivor 区(比如 S0)中所有存活的对象。
- 将这些存活的对象 一次性复制到另一个空的 Survivor 区(比如 S1)。这个“复制”过程天然地完成了内存的整理,使得 S1 区的一端是紧密排列的存活对象,另一端是完整的空闲内存。
- 直接清空 Eden 区和刚使用过的 Survivor 区(S0)。注意,清除是通过复制存活对象、直接覆盖原区域来实现的,速度极快。
- 此时,S1 区成为了下一次回收时的“来源”区(From Survivor)。
- 对象在 Survivor 区之间每经历一次 Minor GC 且存活,其“年龄”就增加 1。当年龄超过阈值(例如 15),在下一次 Minor GC 时,它就会被晋升到老年代。
-
老年代回收(Major GC / Full GC):
- 当老年代的内存空间也被填满时,会触发针对整个堆的垃圾回收,称为 Major GC 或 Full GC。
- Full GC 会同时回收年轻代和老年代,通常使用与年轻代不同的算法(例如“标记-清除-整理”)。
- 由于需要处理整个堆,且老年代存活对象多,Full GC 的 暂停时间通常比 Minor GC 长得多,对应用性能的影响显著。因此,GC 调优的一个重要目标就是减少 Full GC 的发生频率。
2.4 分代设计的优势
- 高效利用局部性:高频的 Minor GC 只扫描和复制年轻代,而年轻代通常只占整个堆的较小部分(例如 1/3),因此回收速度很快,应用暂停时间短。
- 针对性算法:对死亡率高的年轻代,采用高效的 “复制”算法,用空间换时间,且无内存碎片。对存活率高的老年代,采用更节省空间的 “标记-清除”或“标记-整理”算法。
- 吞吐量提升:将回收工作集中在产出垃圾最多的区域,使得单位时间内回收的内存效率最高,整体应用吞吐量得到提升。
第三部分:主流垃圾回收器演进 —— CMS、G1 与 ZGC
现代垃圾回收器的演进史,本质上是工程师们为解决一个核心矛盾而奋斗的历史:如何在不牺牲吞吐量的前提下,尽可能地减少垃圾回收带来的应用暂停时间(Stop-The-World, STW)。从 CMS 到 G1,再到 ZGC,每一代都代表了不同的设计哲学和权衡艺术。
3.1 并发标记清除回收器
设计目标与定位
CMS 是历史上第一个真正意义上追求 低延迟 的垃圾回收器。它的核心目标是减少老年代回收带来的长暂停,通过与应用程序线程并发工作来实现。
工作原理(四阶段)
CMS 的回收过程不再是简单的一次性 “标记-清除”,而是分为四个主要阶段,其中两个阶段是与应用线程 并发 进行的:
- 初始标记:STW 阶段。但速度极快,仅标记从 GC Roots 直接可达 的老年代对象。
- 并发标记:并发阶段。从初始标记的对象出发,遍历整个老年代对象图,标记所有可达对象。此阶段与应用线程一起运行。
- 重新标记:STW 阶段。用于修正 并发标记 阶段中,因应用线程继续运行而导致引用关系发生变化的那部分对象。该阶段使用增量更新算法,速度比初始标记慢,但远快于扫描整个老年代。
- 并发清除:并发阶段。回收那些在标记阶段被判定为不可达的对象所占用的内存空间。
核心优势与代价
- 优势:将最耗时的遍历和清除工作与应用程序并发执行,显著降低了老年代回收的 STW 停顿时间。
- 代价:
- 吞吐量降低:并发执行会占用一部分CPU资源,从而挤占应用线程。
- 浮动垃圾:并发清除阶段,应用线程可能继续产生垃圾,这些垃圾只能等到下次回收。
- 内存碎片:使用“标记-清除”算法,不整理内存,长期运行后可能因碎片过多触发一次耗时的 Full GC。
- 对CPU资源敏感:在CPU核心数少的情况下,并发争抢会严重影响性能。
CMS 标志着 GC 设计从单纯追求吞吐量转向关注延迟,但其架构的固有缺陷使其最终被更先进的回收器取代。
3.2 垃圾优先回收器
设计理念的革新:化整为零
G1 放弃了传统的、物理上连续固定的年轻代和老年代划分。它将整个堆内存划分为多个大小相等(如1MB至32MB)的 区域。每个区域在逻辑上被动态地赋予角色:Eden区、Survivor区、Old区,还有一个特殊的 大对象区域。
核心工作流程:收集集合
G1 不再进行全堆扫描或整个老年代的回收。它的回收过程称为 “混合回收” ,其核心是 回收价值最大化 :
- 后台并发标记:类似于CMS,进行全局并发标记,以识别出哪些区域是“完全空闲的”,哪些区域是“包含大量垃圾的”。
- 筛选回收:G1 会计算每个区域的“回收价值”(即回收该区域能获得多少空间、需要多少时间),并优先选择 垃圾比例最高、回收收益最大 的若干个区域组成 “收集集合” 。
- 转移存活对象:STW 阶段。将 CSet 中所有存活的对象 复制 到其他空闲区域,然后直接清空整个 CSet 区域。这个过程实现了 “标记-复制” 的效果,天然地整理了内存,避免了碎片。
G1 的关键进步
- 可预测的停顿模型:允许用户设置期望的最大暂停时间目标,G1 会通过调整每次回收的 CSet 大小(即回收区域的数量)来尽力满足。
- 整体效率更高:既在年轻代(由多个 Eden 和 Survivor 区域组成)使用高效的复制算法,又能以部分区域为粒度管理老年代,兼顾了吞吐量和延迟。
- 有效控制碎片:通过跨区域的复制整理,有效避免了内存碎片问题。
G1 成为了一个在吞吐量和延迟之间取得更好平衡的 通用型 回收器,是 JDK 9 及以后的默认回收器。
3.3 Z垃圾回收器
设计目标:突破延迟极限
ZGC 是为了应对大数据、实时分析等场景下,对 超大堆内存(TB级别) 和 超低停顿(10毫秒以内) 的极端需求而设计的。它的目标是几乎完全消除垃圾回收停顿对应用响应时间的影响。
核心技术:染色指针与读屏障
ZGC 实现革命性突破的关键在于两项技术创新:
- 染色指针:ZGC 将对象的元数据信息(如标记状态、转发地址)存储在对象指针本身的最高几位中,而不是存储在对象头里。这意味着一个对象在内存中的“位置”不仅包含地址,还包含了GC需要的状态信息。
- 读屏障:这是由 JVM 在对象访问时自动插入的一小段代码。当应用线程从堆中读取一个对象的引用时,读屏障会被触发。它的核心作用是 “解引用”:检查染色指针的状态,如果发现该对象正在被转移,读屏障会确保线程拿到的是转移后的新地址,并可能“帮忙”完成转移工作。正是通过读屏障,ZGC 得以将耗时的对象转移阶段与应用线程并发执行。
工作阶段
ZGC 的回收周期同样是并发的,主要分为:
- 并发标记:遍历对象图,标记存活对象。
- 并发预备重分配:确定本次要清理哪些区域。
- 并发重分配:核心阶段。将存活对象复制到新区域。这是最耗时的部分,但得益于染色指针和读屏障,完全与应用程序并发进行。
- 并发重映射:修正所有指向旧对象地址的指针,使其指向新地址。
ZGC 的优势与现状
- 优势:实现了 亚毫秒级到十毫秒级 的 STW 停顿,且停顿时间不随堆大小增长而增加。支持 TB 级堆内存。
- 现状:在 JDK 15 中成为生产可用特性。它主要牺牲了一定的吞吐量(读屏障开销)来换取极致的低延迟,适用于对延迟极度敏感的应用。
演进总结
从 CMS 到 G1 再到 ZGC,我们看到一条清晰的技术演进路径:
- CMS:“我要并发” —— 开创了并发回收的道路,但留下了碎片和复杂度的问题。
- G1:“我要可预测” —— 通过区域化分代和基于价值的回收,提供了更好的综合性能和可预测的停顿。
- ZGC:“我要极致” —— 利用染色指针和读屏障等全新硬件/软件协同设计,将延迟目标推向了物理极限。
选择哪种回收器,取决于应用的核心诉求:是高吞吐、可预测的短暂停顿,还是极致的低延迟。理解它们的原理,是做出正确选择的第一步。
总结:垃圾回收器的演进与权衡的艺术
回顾我们对垃圾回收机制的探讨,可以看到一条清晰的技术演进路径,其核心驱动力始终围绕着计算机科学中的一个经典三角难题:在内存(空间)、吞吐量(计算资源)和延迟(响应时间)之间,如何取得最符合场景需求的平衡。
下表总结了三大主流回收器的核心权衡:
| 特性 | CMS (并发标记清除) | G1 (垃圾优先) | ZGC (Z垃圾回收器) |
|---|---|---|---|
| 主要目标 | 降低老年代回收的延迟 | 在可预测的停顿下,兼顾吞吐与延迟 | 极致低延迟,支持超大堆 |
| 堆布局 | 物理连续的年轻代/老年代 | 分区的堆 (Region) | 分区的堆 (Region) |
| 核心算法 | 并发标记-清除 | 标记-复制 (以Region为单位) | 并发标记-复制 |
| 停顿时间 | 中等,初始标记与重新标记阶段STW | 可预测,通过调整CSet大小控制 | 极短且固定 (<10ms),与堆大小无关 |
| 内存碎片 | 严重(标记-清除算法) | 很少(复制算法整理) | 无(并发复制整理) |
| 吞吐量影响 | 明显(并发争抢CPU) | 中等 | 较高(读屏障开销) |
| 适用场景 | 对延迟敏感、CPU资源充足的中小规模应用 | JDK9+默认,通用服务器应用,需平衡吞吐与延迟 | 超大规模内存、对延迟极端敏感的应用(如实时分析、金融交易) |
更多推荐




所有评论(0)