垃圾回收(Garbage Collection, GC)是 JVM 自动化内存管理的核心,也是 Java 程序员区别于 C/C++ 程序员的幸福所在。然而,在面对高并发、大流量的生产环境时,GC 引发的全局停顿(STW, Stop-The-World)往往是导致系统卡顿、接口超时的罪魁祸首。理解 GC 的底层原理与经典收集器的演进,是进行极限性能调优的必备基本功。本文将带你深入 JVM 的垃圾回收机制,从基础算法一路打通至 ZGC 的黑科技。
一、 垃圾的判别:生死宣判的底层规则
要回收垃圾,首先要准确地找出内存中哪些对象已经“死亡”。JVM 摒弃了存在循环引用致命缺陷的“引用计数法”,全面采用了可达性分析算法 (Reachability Analysis)。
1. 可达性分析的核心逻辑
该算法引入了一系列被称为 GC Roots(根节点) 的对象作为绝对安全的起始点,向下顺藤摸瓜搜索引用的对象。搜索走过的路径称为引用链。
-
可达对象:有链路连向 GC Roots,判定为存活。
-
不可达对象:孤立在引用链之外,即使对象之间有互相引用(循环引用),只要连不到 Roots,统统判定为垃圾。
2. 🔥 面试必背:谁有资格做 GC Roots?
为了保证内存的安全清算,JVM 将以下四类处于“绝对活跃状态”的对象设为根:
-
虚拟机栈(栈帧中的局部变量表)中引用的对象:正在运行的方法中的局部变量、参数。
-
方法区中类的静态属性、常量引用的对象:它们随类的加载而存在,生命周期长,是稳定的引用源。
-
本地方法栈中 JNI(Native 方法)引用的对象:跨语言调用时的安全保障。
-
所有被同步锁(
synchronized)持有的对象:正在被多线程竞争或使用的资源,绝对不可回收。
二、 三大基础清理算法:时间与空间的博弈
找到垃圾后,JVM 演进出了三种核心清理算法,它们是现代复杂垃圾回收器的基石:
| 算法名称 | 核心操作逻辑 | 致命痛点 | 核心适用场景 |
| 标记-清除 (Mark-Sweep) | 先标记出所有垃圾,然后直接打扫原地清除。 | 会产生大量不连续的内存碎片。大对象分配时易提前触发 GC。 | CMS 的老年代回收。 |
| 标记-整理 (Mark-Compact) | 标记后,将所有存活对象向内存一端移动排齐,直接切掉边界外垃圾。 | 涉及大量对象的物理移动并需更新引用地址,效率极低,STW 时间长。 | 老年代的兜底回收 (如 Serial Old)。 |
| 复制算法 (Copying) | 将内存划分为两块,每次只用一块;满后将存活对象搬家到另一块,一次性清空原内存。 | 极度浪费空间,内存可用率直接减半(未优化前)。 | 存活率极低的年轻代(优化为 8:1:1 结构)。 |
三、 并发 GC 的底层基石:三色标记法与漏标危机
传统的标记过程需要全程 STW(暂停所有用户线程),导致系统停顿时间过长。为了让垃圾回收线程与用户线程并发执行,CMS 和 G1 底层都采用了三色标记法。
1. 颜色的定义
三色标记法将内存中的对象按“扫描进度”分为三种颜色:
-
⚪ 白色(未被标记):尚未被垃圾回收器访问过。若标记结束仍为白色,则判定为垃圾。
-
🔘 灰色(标记进行中):该对象已被访问过,但它引用的子对象还没有被完全扫描完。
-
⚫ 黑色(标记已完成):该对象及其所有引用的子对象都已被扫描完毕,绝对安全。
2. 致命危机:漏标(对象消失)问题 🔥
当用户线程在 GC 并发标记期间修改了引用关系,活对象可能会被误当成垃圾清理掉,导致系统崩溃。发生漏标必须同时满足以下两个条件:
-
黑连白:黑色对象新增了一条或多条指向白色对象的引用。
-
灰断白:灰色对象删除了指向该白色对象的所有直接或间接引用。
3. 工业级破局方案:CMS vs G1
只要破坏上述任意一个条件,就能解决漏标:
-
CMS 的解法:增量更新 (Incremental Update)
-
思路:破坏条件 1(针对新增)。
-
机制:一旦黑色对象指向了白色对象,就把这个黑色对象记录下来;在重新标记阶段,把它降级回灰色重新扫描一遍。
-
-
G1 的解法:原始快照 (SATB, Snapshot At The Beginning)
-
思路:破坏条件 2(针对删除)。
-
机制:一旦灰色对象断开了对白色对象的引用,就把这个白色对象记录下来;在最终标记阶段,强行将其标记为黑色(存活),“宁可错放产生浮动垃圾,也绝不漏标错杀”。
-
四、 工业级双雄对比:CMS 与 G1 的架构博弈
1. CMS (Concurrent Mark Sweep) —— 响应时间优先的老派王者
CMS 针对老年代设计,极力减少 STW 时间,适用于对延迟敏感的 C 端系统。
-
核心四步走:
-
初始标记 (STW):仅标记 GC Roots 直接关联对象,极快。
-
并发标记 (并发):顺藤摸瓜遍历对象图,耗时最长但不卡顿。
-
重新标记 (STW):处理并发阶段产生的漏标变动。
-
并发清理 (并发):采用标记-清除算法清理垃圾。
-
-
两大致命痛点:
-
内存碎片陷阱:因采用标记-清除,老年代会布满碎片。遇到大对象无法分配时,会退化为 Serial Old 单线程收集器,引发长达秒级的恐怖 STW。
-
浮动垃圾:并发清理时用户线程产生的“新垃圾”无法当次处理,若浮动垃圾撑爆老年代(Concurrent Mode Failure),同样会导致系统崩溃。
-
2. G1 (Garbage-First) —— 现代化可预测停顿军团
G1 面向大内存服务器,追求吞吐量与低延迟的完美平衡,并在 JDK 9 之后成为默认收集器。
-
颠覆性架构:Region 分区:打破了年轻代与老年代的物理隔离,将整个堆划分为几百上千个大小相等的 Region(区域)。每个 Region 动态扮演 Eden, Survivor, Old 或存放超大对象的 Humongous 角色。
-
核心优势:可预测停顿与无碎片:
-
G1 每次不会扫描全堆,而是根据用户设定的期望停顿时间(如 200ms),优先挑选垃圾最多、收益最大的 Region 进行回收(Garbage-First 的由来)。
-
采用复制算法进行 Region 间的对象搬运清理,完美解决了 CMS 的内存碎片痛点。
-
五、 GC 的未来:ZGC 超低延迟黑科技深度剖析
ZGC 是 JDK 11+ 引入的划时代收集器,打破了“堆内存越大,GC 停顿越久”的物理魔咒。
-
恐怖指标:无论堆内存是 8MB 还是 16TB,ZGC 都能将 STW 停顿时间永远控制在 1ms 以下(JDK 16+)。
-
核心黑科技(面试绝杀):
-
染色指针 (Colored Pointers):ZGC 不去对象头里存 GC 状态,而是直接把状态信息(如对象是否被移动过)写在 64 位指针的高位地址里!读取指针的瞬间就能知晓对象状态,速度极快。
-
读屏障 (Load Barrier) 与指针自愈 (Self-Healing):当存活对象被 GC 搬家后,如果有用户线程拿着旧地址去访问,底层的“读屏障”会瞬间拦截。它通过转发表查到新地址返回给用户,并顺手把旧指针改写成新指针(指针自愈)。第二次访问时直接就是新地址,毫无卡顿,彻底免去了 G1 那种需要 STW 来统一更新引用的代价。
-
六、 线上 GC 触发条件与选型指南
1. 垃圾回收器选型四大维度
| 维度考量 | 推荐选型 |
| 单核/微小内存 | Serial 家族(避免多线程上下文切换开销)。 |
| 吞吐量优先 (如后台跑批) | Parallel Scavenge + Parallel Old (允许较长 STW 换取极致计算性能)。 |
| 低延迟 & 小内存 (< 4GB/8GB) | CMS (因 G1 维护 Region 元数据耗费较多内存,小内存下负担重)。 |
| 低延迟 & 大内存 (8GB+) | G1 (利用分区回收优势,避免大内存的全堆扫描)。 |
| 极致毫秒级延迟 | ZGC (需 JDK 11+ 环境)。 |
2. Full GC 核心触发场景汇总
坚决避免 Full GC 是调优的终极 KPI。以下情况会强制触发全堆清理:
-
老年代空间不足:经过多次 Minor GC 晋升或大对象直达,把老年代塞满。
-
碎片严重:CMS 产生大量碎片,导致大对象无法找到连续空间分配。
-
空间分配担保失败:Minor GC 前发现老年代最大可用连续空间小于历次晋升平均大小。
-
元空间 (Metaspace) 爆满:动态加载的类过多达到阈值。
-
显式调用
System.gc():代码中手动建议触发(极不推荐)。
&spm=1001.2101.3001.5002&articleId=161461151&d=1&t=3&u=f414f33378c343c083d251b1da77f5a2)

被折叠的 条评论
为什么被折叠?



