垃圾回收算法有哪些?

2025年 阅读约 8 分钟 面试指南 · Java面试 · JVM

深入解析JVM四种垃圾回收算法:标记-清除(Mark-Sweep)、标记-复制(Mark-Copy)、标记-整理(Mark-Compact)、分代收集(Generational Collection),附完整算法对比、优缺点分析和面试模拟。

一句话总结

JVM 垃圾回收有四种基础算法:标记-清除(Mark-Sweep,标记存活对象后清除未标记的,有碎片问题)、标记-复制(Mark-Copy,将存活对象复制到另一块区域,浪费一半内存但无碎片,适合新生代)、标记-整理(Mark-Compact,标记后将存活对象向一端移动,无碎片但耗时,适合老年代)、分代收集(Generational Collection,新生代用复制算法,老年代用标记-清除/整理,是当前 JVM 的实际策略)。

初级理解

四种算法对比

算法过程优点缺点适用
标记-清除标记存活 → 清除未标记简单碎片多、效率低老年代(CMS)
标记-复制标记存活 → 复制到新区域无碎片、效率高浪费一半内存新生代
标记-整理标记存活 → 向一端移动无碎片移动对象耗时老年代
分代收集组合以上算法各取所长实现复杂当前 JVM 默认

如何判断对象可回收?

// ① 引用计数法(Java 不用,有循环引用问题) class A { B b; } class B { A a; } // A 引用 B,B 引用 A,引用计数都不为 0,但实际已不可达 // ② 可达性分析(Java 使用) // 从 GC Roots 出发,通过引用链无法到达的对象 = 可回收 // GC Roots 包括: // - 虚拟机栈中引用的对象 // - 方法区中静态属性引用的对象 // - 方法区中常量引用的对象 // - 本地方法栈中 JNI 引用的对象 // - 被 synchronized 持有的对象

中级深入

标记-复制算法在新生代的优化

新生代不按 1:1 分配,而是 Eden:S0:S1 = 8:1:1。每次只使用 Eden + 一个 Survivor(共 90%),GC 时将存活对象复制到另一个 Survivor。如果 Survivor 放不下,通过分配担保直接进入老年代。实际只浪费 10% 内存。

// 新生代 Minor GC 过程 // ① Eden + S0 中的存活对象 → 复制到 S1 // ② 清空 Eden + S0 // ③ 下次 GC:Eden + S1 → 复制到 S0,交替使用 // 对象晋升老年代的条件: // 1. 年龄计数器达到 -XX:MaxTenuringThreshold(默认 15) // 2. Survivor 中相同年龄的对象大小超过 Survivor 一半 → 该年龄及以上直接晋升 // 3. 大对象直接进入老年代(-XX:PretenureSizeThreshold)

三色标记法

// CMS 和 G1 使用三色标记法处理并发标记 // 白色:未被标记,GC 结束后被回收 // 灰色:自身被标记,但引用的子对象未全部标记 // 黑色:自身和所有子对象都已标记 // 并发标记的问题: // ① 浮动垃圾:黑色对象引用被断开,本次 GC 无法回收(下次回收) // ② 漏标:需要同时满足两个条件才会漏标 // - 灰色对象断开了对白色对象的引用(插入屏障解决) // - 黑色对象新增了对白色对象的引用(删除屏障/SATB 解决)

高级拓展

跨代引用和记忆集

新生代 GC 时,老年代可能引用新生代对象(跨代引用)。如果扫描整个老年代效率太低,JVM 使用记忆集(Remembered Set)记录老年代中哪些区域引用了新生代对象。GC 时只需扫描记忆集记录的区域。

// 卡表(Card Table):记忆集的一种实现 // 将老年代内存划分为 512 字节的"卡" // 如果某张卡中的对象引用了新生代对象,该卡标记为"脏卡" // Minor GC 时只扫描脏卡,大幅减少扫描范围 // 写屏障(Write Barrier):维护卡表 // 每次引用赋值时,通过写屏障检查并更新卡表

Stop The World(STW)

GC 过程中需要暂停所有用户线程,称为 STW。不同算法和收集器的 STW 时间不同:Serial 最长,CMS 和 G1 通过并发标记减少 STW,ZGC 和 Shenandoah 目标是将 STW 控制在 10ms 以内。

实战场景

场景:GC 日志分析

# 开启 GC 日志(JDK 8) -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log # GC 日志示例 2025-01-01T10:00:00.000+0800: 1.234: [GC (Allocation Failure) [PSYoungGen: 65536K->1024K(76288K)] 65536K->2048K(251392K), 0.0034567 secs] # 解读: # PSYoungGen:新生代使用 Parallel Scavenge # 65536K->1024K:GC 前后新生代使用量 # (76288K):新生代总大小 # 65536K->2048K:GC 前后堆总使用量 # (251392K):堆总大小 # 0.0034567 secs:GC 耗时

面试模拟

Q:垃圾回收算法有哪些?各自优缺点?

A:标记-清除:简单但有碎片;标记-复制:无碎片效率高但浪费内存,适合新生代(对象朝生夕死);标记-整理:无碎片但移动对象耗时,适合老年代;分代收集:组合以上算法,新生代用复制(Eden:S0:S1=8:1:1),老年代用标记-清除或标记-整理。

Q:如何判断对象是否可回收?

A:Java 使用可达性分析,从 GC Roots 出发,通过引用链无法到达的对象判定为可回收。GC Roots 包括:虚拟机栈中引用的对象、静态属性引用的对象、常量引用的对象、JNI 引用的对象、synchronized 持有的对象。引用计数法有循环引用问题,Java 不用。

Q:为什么新生代用复制算法,老年代用标记-整理?

A:新生代对象朝生夕死,存活率低,复制算法只需复制少量存活对象,效率高。老年代对象存活率高,如果用复制算法需要大量复制操作且浪费一半内存,所以用标记-清除或标记-整理。标记-整理比标记-清除多了移动步骤,但消除了碎片。