JVM篇2:根可达性算法-垃圾回收算法和三色标记算法-CMS和G1

作为 Java 开发者,我们享受着自动内存管理的便利,但面试和线上调优时,JVM 的垃圾回收(GC)往往是绕不开的大山。

很多人背八股文时会感到割裂:一会儿是"标记清除算法",一会儿是"三色标记",一会儿又是"CMS/G1",它们到底是什么关系?

本文将自顶向下,帮你构建一张完整的 JVM 垃圾回收知识图谱。

前置知识:核心概念综述,从理论到实践

在深入探讨具体的算法实现之前,我们需要构建一个关于 Java 垃圾回收(GC)的宏观知识图谱。我们将从判定标准回收策略落地实现 以及调优演进四个维度来拆解 GC 体系。

1. 判定标准:什么是垃圾?

GC 的首要任务是判断哪些对象是"存活"的,哪些是"垃圾"。

  • 早期探索(引用计数法) :通过计算对象被引用的次数来判断。但它有一个致命缺陷------无法解决循环引用(A 引用 B,B 引用 A,两者都无外部引用)导致的内存泄漏问题,因此已被现代 JVM 抛弃。

  • 现代标准(根可达性算法) :这是 Java 目前使用的核心法则。以一系列 GC Roots (如栈中引用的对象、静态变量等)为起点,沿着引用链向下搜索。凡是不可达的对象,无论它们之间是否有连接,均被视为垃圾。

2. 核心策略:垃圾回收算法

确定了垃圾之后,我们需要高效地清理它们。主流的基础算法有三种,它们各有优劣:

  • 标记-清除 (Mark-Sweep)

    • 特点:直接标记出垃圾并清除。

    • 优点:速度快,基础简单。

    • 缺点 :会产生大量内存碎片,导致后续分配大对象时可能因无法找到连续空间而触发 GC。

  • 标记-整理 (Mark-Compact)

    • 特点:清除垃圾后,将存活对象向一端移动、整理。

    • 优点无内存碎片

    • 缺点:涉及对象的移动,效率相对较低。

  • 复制算法 (Copying)

    • 特点:将内存分为两块,每次只用一块。回收时将活对象复制到另一块,清空当前块。

    • 优点 :运行高效,且无内存碎片

    • 缺点空间利用率低,需要占用双倍内存空间(以空间换时间)。

3. 管理思想:分代垃圾回收

在实际应用中,JVM 不会单一地使用某种算法,而是基于**"对象生命周期不同"这一事实,采用了分代管理**策略:

  • 年轻代 (Young Gen)

    • 特点:存放"朝生夕死"的对象,GC 频率极高。

    • 策略 :采用 复制算法。因为存活对象少,复制成本低,效率最高。

  • 老年代 (Old Gen)

    • 特点:存放生命周期长的对象,GC 频率较低。

    • 策略 :采用 标记-整理标记-清除 算法。因为对象存活率高,不适合用复制算法(空间浪费且复制开销大)。

4. 落地执行:垃圾回收器

垃圾回收器是上述算法的具体实现者。不同的回收器适用于不同的业务场景:

  • CMS (Concurrent Mark Sweep)

    • 定位 :老年代收集器,响应时间优先(低延迟)。

    • 核心机制 :基于标记-清除算法。

    • 亮点 :在最耗时的"并发标记"和"并发清理"阶段,允许 GC 线程与用户线程同时工作,从而极大降低了 STW(Stop The World)停顿时间。

  • G1 (Garbage First)

    • 定位 :全年代收集器,面向大内存,追求可预测的停顿时间

    • 核心机制 :打破了物理分代,将堆划分为多个大小相等的 Region

    • 亮点 :它能根据用户设定的目标(如 STW 不超过 200ms),优先回收垃圾收益最高的 Region。这使得它在控制停顿时间的同时,保持了较高的吞吐量。

5. 演进与调优:GC 的自我修养

理解了回收器后,我们需要根据系统特性进行选择和调优:

  • 回收器的选择

    • 标准吞吐量 :JDK 8 默认使用 Parallel GC

    • 低延迟要求 :如果系统对响应时间敏感,可切换为 CMS

    • 大内存 & 可控停顿 :如果拥有超大堆内存且需要精准控制 STW 时间,G1 是最佳选择。

  • JVM 内部优化

    • 元空间 (Metaspace):JDK 8 将方法区从堆内存(永久代)移到了本地内存(元空间)。这是为了适应动态类加载技术的发展,避免因永久代大小固定导致的 OOM 问题。

    • 减负手段 :通过逃逸分析栈上分配技术,让未逃逸的对象直接在栈上分配并销毁,从而减少堆内存的分配压力和 GC 频率。

一、 顶层视角:它们到底是什么关系?

在深入细节前,我们需要先上帝视角看清各个概念在 JVM 体系中的层次

  • 根可达性算法 :这是宪法。它规定了"什么是垃圾,什么是活人"。所有的回收器必须遵守这个标准。

  • 基础回收算法(标记清除/复制/整理) :这是战略。它是处理垃圾的宏观策略(是直接扔、还是搬家、还是整理)。

  • 三色标记法 :这是战术。它专门解决**"并发"**问题。当垃圾回收线程和用户线程同时运行时,如何保证标记不乱套?靠的就是三色标记。

  • 垃圾回收器(CMS/G1) :这是执行者(士兵)。它们是上述理论的具体代码实现。

核心层次图

要弄明白垃圾回收,理解他们之间的层次关系至关重要。

一句话总结:

CMS 和 G1 是两名不同的清洁工,他们都遵守根可达性(宪法)来判断垃圾。为了能在大家工作时不关门打扫(并发),他们都用到了三色标记(战术)。只不过 CMS 习惯用标记-清除(战略),而 G1 习惯用标记-整理/复制(战略)。


二、 判决标准:根可达性算法 (Reachability Analysis)

在 Java 中,我们早已抛弃了"引用计数法",而是采用根可达性算法来判断对象死活。

1. 核心逻辑

JVM 会寻找一批"绝对不能回收"的对象作为起点,称为 GC Roots

  • 从这些起点开始向下搜索,连在一起的(引用链上)就是活的

  • 断开链接的,哪怕它们之间互相引用,也是垃圾

2. 谁是 GC Roots?(记四类即可)

  • 栈中引用的对象 (比如你正在执行的方法里的 User user = ...)。

  • 方法区中静态变量引用的对象static 修饰的)。

  • 方法区中常量引用的对象final 修饰的)。

  • 本地方法栈中引用的对象(Native 方法)。

  • 被同步锁持有的对象synchronized 锁住的)。


三、 并发标记的核心:三色标记法 (Tri-color Marking)

正常的标记过程,应该是需要STW的,对象的引用关系是不会变化的,才能标记正确

但是这个标记过程需要消耗时间,可能导致STW的时间很长。

因此为了减少STW的时间,才出现了三色标记法,它允许并发标记,允许用户线程工作

1. 三种颜色的含义

我们将对象分为三种状态:

  • ⚪ 白色未被访问。如果扫描结束还是白色,说明是垃圾(死罪)。

  • 🔘 灰色正在访问。自己被标记了,但引用的子对象还没检查完。(任务队列)。

  • ⚫ 黑色访问完毕。自己和引用的子对象都检查完了。GC 以后不会再理它(免死金牌)。

2. 并发下的难题:漏标与多标

在用户线程一边跑、GC 一边标的过程中,会发生两种情况:

A. 多标(浮动垃圾):多标,这个标就相当于一个免死金牌,也就是多给他上了一个免死金牌,导致它本来应该是垃圾却没有被回收。
  • 现象:对象 A 已经是黑色了,结果用户突然把 A 的引用断开了。

  • 结果:A 本来该死,但因为已经是黑色,GC 这轮不杀它。

  • 影响无害。多占点内存而已,下次 GC 再杀。

B. 漏标(对象消失 - 致命 BUG):漏标,这个标也同样是免死金牌,漏掉了这个免死金牌,导致它本不应该被回收的却被回收了。
  • 现象本来活着的对象,被当成白色垃圾杀了。

  • 成因(必须同时满足)

    1. 灰色断开:灰色对象断开了对白色对象的引用。

    2. 黑色指向:黑色对象(已免检)重新引用了这个白色对象。

  • 结果:白色对象躲到了黑色对象身后,GC 扫描器看不见,直接把它回收了。程序崩溃。

3. 解决方案

CMS 和 G1 分别打破上述两个条件之一来解决漏标:

  • CMS (增量更新) :关注新增 。只要黑色对象指向了白色,把黑色变回灰色。下次重新扫描它。(针对漏标的条件1)

  • G1 (原始快照 SATB) :关注删除 。只要灰色对象断开了白色,把这个引用记录下来。不管它真的还是假的,这轮 GC 先当它是活的。(针对漏标的条件2)


四、 落地实现:CMS 与 G1 的巅峰对决

理解了底层的算法和三色标记法后,我们来看看 JVM 中最著名的两款垃圾回收器:CMSG1。它们代表了 JVM 垃圾回收技术的两个重要里程碑。

1. CMS (Concurrent Mark Sweep) ------ 老年代的"低延迟专家"

设计目标 : CMS 是第一款真正意义上的并发收集器,它的核心目标是 响应时间优先。它致力于将 STW(Stop The World)停顿时间降到最低,非常适合对延迟敏感、希望用户感知不到卡顿的系统(如 Web 服务器)。

核心原理 : 为了实现低停顿,CMS 采用了 "标记-清除" 算法,并且创新性地实现了让垃圾回收线程与用户线程并发工作

CMS 回收的详细过程: 整个过程分为四个阶段,其中只有两个阶段需要 STW:

  1. 初始标记 (Initial Mark) [需 STW]

    • 仅仅标记 GC Roots 直接关联的对象。

    • 特点:速度极快,因为只查第一层引用。

  2. 并发标记 (Concurrent Mark) [与用户线程并发]

    • 从初始标记的对象出发,遍历整个对象图,标记所有可达的对象。

    • 特点:这是最耗时的阶段,但它与用户线程同时运行,所以用户无感知。

  3. 重新标记 (Remark) [需 STW]

    • 目的 :由于并发标记阶段用户线程还在跑,可能会修改对象的引用关系,导致 "漏标"(即本来活着的对象被当成了垃圾,详见上文三色标记法)。

    • 动作:需要再次暂停,修正这些在并发期间变动的标记记录,确保准确性。

  4. 并发清理 (Concurrent Sweep) [与用户线程并发]

    • 清理掉那些未被标记的垃圾对象。

    • 特点:因为采用"标记-清除"算法,不需要移动存活对象,所以可以并发进行。

CMS 的弊端(阿喀琉斯之踵): 虽然 CMS 实现了低延迟,但它付出了昂贵的代价:

  • 1. 内存碎片问题

    • 原因 :CMS 基于 "标记-清除" 算法。它只管清除垃圾,不进行内存整理

    • 影响:长期运行后,老年代会布满像蜂窝煤一样的碎片。即使总剩余空间很大,但当需要分配一个连续的大对象时,找不到足够大的连续空间,只能被迫触发 Full GC。

  • 2. 浮动垃圾问题

    • 原因 :在 并发清理 阶段,用户线程还在运行,这期间产生的垃圾 CMS 这一轮是看不见的(无法标记),只能留到下一轮处理。这些垃圾被称为"浮动垃圾"。

    • 影响:这要求 CMS 不能像其他收集器那样等老年代满了再回收,必须预留一部分空间给用户线程和浮动垃圾使用。如果预留不够,就会导致并发模式失败。

  • 3. 退化风险 (Concurrent Mode Failure)

    • 当"内存碎片过多"导致大对象分配失败,或者"浮动垃圾过多"导致老年代预留空间不足时,CMS 会宣告失败。

    • 后果 :它会 退化为 Serial Old 垃圾回收器。这是一款单线程、基于"标记-整理"算法的回收器。它会强制所有用户线程暂停(长时间 STW),慢慢整理内存。这是系统卡死的噩梦。


2. G1 (Garbage First) ------ 全年代的"可控管家"

堆划分的革命 (Region): G1 彻底打破了物理分代的界限。

  • 它不再将堆简单地分为一大块新生代和一大块老年代。

  • 它将整个堆内存划分为 2000 多个大小相等的 Region(独立区域)

  • 每个 Region 的身份(Eden、Survivor、Old、Humongous)是动态分配的。

  • 意义"分区"取代了"分代"。算法驱动设计,这种结构让回收策略变得极其灵活。

核心特点:可预测的停顿模型 G1 最大的卖点在于:用户可以指定垃圾回收的 STW 停顿时间目标 (例如:-XX:MaxGCPauseMillis=200,即每次回收尽量不超过 200ms)。

实现原理 : G1 为什么能"说到做到"?这得益于它的 Garbage First(垃圾优先) 策略:

  1. G1 会在后台维护一张表,记录每个 Region 里的"垃圾有多少"以及"回收它需要多久"。

  2. 当进行回收时,G1 不会傻傻地回收整个老年代。它会根据你的时间目标(比如 200ms),只选择回收那些垃圾最多、回收收益最大的 Region。

  3. 效益最大化:回收范围缩小了(省时间),但回收掉的垃圾却很多(高效率),从而完美命中用户设定的暂停目标。

G1 回收的详细过程: G1 的回收过程与 CMS 类似,但在最后一步有本质区别:

  1. 初始标记 (Initial Mark) [需 STW]:标记 GC Roots 直接关联的对象。

  2. 并发标记 (Concurrent Mark) [与用户线程并发]:进行全局的可达性分析。

  3. 最终标记 (Final Mark) [需 STW]:类似于 CMS 的重新标记,处理并发期间引用变动导致的漏标问题(G1 使用 SATB 算法,比 CMS 更快)。

  4. 混合回收 (Mixed GC) [需 STW]

    • 这是 G1 的精髓。它不再只回收年轻代或老年代。

    • 计划 :它会基于暂停时间目标,制定一个回收计划,选择 所有年轻代 Region + 部分收益高的老年代 Region + 大对象 Region

    • 动作 :它采用 标记------复制算法。将这些 Region 里的存活对象复制到空的 Region 中,然后清空原 Region。

    • 结果 :既完成了回收,又顺便进行了内存整理,没有内存碎片

So : G1 是一款集大成者,适用于 拥有超大堆内存 、追求 高吞吐量 且需要 低延迟、可预测暂停时间 的现代系统。它用更先进的 Region 结构和算法,解决了 CMS 难以克服的碎片问题。


五、 总结与对比分析

最后,我们将这两位主角放在一起对比,助你彻底拿下面试题。

维度 CMS 收集器 G1 收集器
适用区域 仅老年代 (因为老年代对象很多占总的2/3) 全堆 (新生代 + 老年代)
内存结构 传统的物理分代 Region 分区 (逻辑分代)
回收算法 标记-清除 (有碎片) 标记-复制/整理 (无碎片)
并发漏标解法 增量更新 (重新扫描黑色对象) SATB (保留快照,不重扫)
STW 控制 尽力而为 (不可控) 精准控制 (用户设定目标)
最大弱点 碎片导致 Full GC 退化 内存占用较高 (跨代引用表)
一句话定位 JDK 8 之前的低延迟首选 JDK 9+ 默认的大内存管家

结语

记住:根可达性是宪法,垃圾回收算法是战略,三色标记是战术,而 CMS 和 G1 则是落地实现。

相关推荐
凌冰_2 小时前
Thymeleaf Maven+Servlet+Mysql图书框架—2(八)
java·mysql·maven
indexsunny2 小时前
互联网大厂Java面试实战:Spring Boot与微服务在电商场景中的应用解析
java·数据库·spring boot·微服务·maven·flyway·电商
sunnyday04262 小时前
从混乱到清晰:Maven 依赖版本管理最佳实践
java·spring boot·后端·maven
roman_日积跬步-终至千里2 小时前
【大数据框架】Calcite 基础概念:从 SQL 到执行计划的思维路径
java·大数据·sql
cypking2 小时前
后端框架搭建完全指南
java
roman_日积跬步-终至千里2 小时前
【SQL】SQL 语句的解析顺序:理解查询执行的逻辑
java·数据库·sql
雨中飘荡的记忆2 小时前
Spring Test 从入门到实战
java·后端·spring
TeamDev2 小时前
JxBrowser 8.16.0 版本发布啦!
java·chromium·浏览器自动化·jxbrowser·浏览器控件·枚举清理·跨配置文件复制密码
毕设源码-钟学长2 小时前
【开题答辩全过程】以 高校体育赛事管理系统的设计与实现为例,包含答辩的问题和答案
java