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