跳到主要内容
EN

垃圾回收算法

9 分钟阅读

垃圾回收概述

垃圾回收(Garbage Collection, GC)是 JVM 自动内存管理的核心机制。GC 需要解决三个基本问题:

  1. 哪些内存需要回收? — 对象存活判定
  2. 什么时候回收? — GC 触发时机
  3. 如何回收? — 垃圾回收算法

对象存活判定

引用计数法

给对象添加一个引用计数器,每被引用一次加 1,引用失效减 1,为 0 时回收。

致命缺陷:无法解决循环引用。

Object a = new Object();
Object b = new Object();
a.field = b;
b.field = a;
a = null;
b = null;
// 两个对象互相引用,计数器不为 0,但实际已不可达

JVM 不使用引用计数法,而是使用可达性分析。

可达性分析算法

GC Roots 出发,沿引用链向下搜索,搜索走过的路径称为引用链(Reference Chain)。当一个对象到 GC Roots 没有任何引用链相连时,证明对象不可达,可回收。

GC Roots ──→ A ──→ B ──→ D
              │
              └──→ C ──→ E

     F ──→ G (不可达,可回收)

GC Roots

可作为 GC Roots 的对象包括:

来源 说明
虚拟机栈中引用的对象 各栈帧的局部变量表中的引用
方法区中类静态属性引用的对象 static 修饰的变量
方法区中常量引用的对象 final static 修饰的变量
本地方法栈中 JNI 引用的对象 native 方法引用的对象
JVM 内部引用 基本类型对应的 Class 对象、常驻异常、类加载器等
同步锁持有的对象 synchronized 锁住的对象
JMXBean、JVMTI 中注册的回调 运行时管理接口引用

四种引用类型

JDK 1.2 之后将引用分为四种:

引用类型 特点 回收时机 用途
强引用(Strong) Object obj = new Object() 永不回收(只要引用存在) 普通对象引用
软引用(Soft) SoftReference<T> 内存不足时回收 缓存
弱引用(Weak) WeakReference<T> 下次 GC 时回收 WeakHashMap、ThreadLocal
虚引用(Phantom) PhantomReference<T> 随时回收,仅用于跟踪 GC 跟踪 GC、管理堆外内存
// 软引用示例 - 适合做缓存
SoftReference<byte[]> cache = new SoftReference<>(new byte[1024 * 1024]);
byte[] data = cache.get();  // 可能返回 null(如果已被 GC 回收)

// 弱引用示例 - 适合做临时映射
WeakReference<Key> ref = new WeakReference<>(key);

// 虚引用示例 - 配合 ReferenceQueue 使用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(obj, queue);
// 对象被回收后,虚引用入队,可在此清理堆外内存

基础垃圾回收算法

标记-清除算法(Mark-Sweep)

最基础的收集算法,分两个阶段:

  1. 标记:从 GC Roots 出发,标记所有可达对象
  2. 清除:遍历堆,回收未标记的对象
标记前:  [A] [B] [C] [D] [E] [F]
                ↑       ↑   ↑
              可达    可达  可达

标记后:  [A✓] [B] [C✓] [D] [E✓] [F]

清除后:  [  ] [C✓] [  ] [E✓] [  ]  ← 产生内存碎片
  • 优点:实现简单,不需要移动对象
  • 缺点
    • 产生大量不连续的内存碎片
    • 分配大对象时可能触发另一次 GC
    • 标记和清除效率都不高

复制算法(Copying)

将内存分为两块等大小区域,每次只使用其中一块:

  1. 将存活对象复制到另一块区域
  2. 清除当前区域所有对象
  3. 交换两块区域的角色
From 区:  [A✓] [B] [C✓] [D] [E✓] [F]
                     ↓ 复制
To 区:   [A] [C] [E] [              ]  ← 连续空间,无碎片
  • 优点:实现简单,运行高效,无内存碎片
  • 缺点:可用内存缩小为一半,空间利用率低

优化:新生代中 98% 的对象都是”朝生夕死”的,不需要 1:1 划分。HotSpot 默认 Eden:S0:S1 = 8:1:1,只浪费 10% 的空间。

标记-整理算法(Mark-Compact)

结合标记-清除和复制算法的优点:

  1. 标记:与标记-清除相同
  2. 整理:将所有存活对象向一端移动,然后清理边界以外的内存
标记后:  [A✓] [B] [C✓] [D] [E✓] [F]

整理后:  [A] [C] [E] [              ]  ← 连续空间,无碎片
  • 优点:无内存碎片,不需要牺牲额外空间
  • 缺点:移动对象需要更新引用,效率较低(尤其是存活对象多时)

算法对比

算法 标记-清除 复制 标记-整理
速度 中等 最快 最慢
空间开销 少(有碎片) 大(2 倍空间) 少(无碎片)
移动对象
适用场景 老年代(CMS) 新生代 老年代

分代收集理论

理论基础

分代收集基于两个假说:

  1. 弱分代假说:绝大多数对象都是朝生夕死的
  2. 强分代假说:熬过越多次 GC 的对象越难被回收

基于这两个假说,JVM 将堆分为新生代和老年代,对不同代采用不同的收集算法:

  • 新生代:对象存活率低,适合复制算法(只需复制少量存活对象)
  • 老年代:对象存活率高,适合标记-清除或标记-整理算法

跨代引用

跨代引用(老年代引用新生代,或反过来)是个问题。如果 Minor GC 只扫描新生代,如何确保不误回收被老年代引用的新生代对象?

解决方案:记忆集(Remembered Set)

记忆集用于记录从非收集区域指向收集区域的指针。在新生代 GC 时,只需扫描记忆集而非整个老年代。

HotSpot 使用卡表(Card Table)实现记忆集:将老年代划分为固定大小的卡页(512 字节),每个卡页对应卡表中一个字节。当老年代中某个卡页的引用字段被修改时,卡表对应位置被标记为”脏”。

卡表标记:  [0] [0] [1] [0] [0] [1] [0] [0]
                  ↑           ↑
               脏页          脏页
            (有跨代引用)  (有跨代引用)

写屏障(Write Barrier)用于维护卡表,在引用赋值时更新卡表状态。

安全点与安全区域

安全点(Safepoint)

GC 需要确保在一致性状态下进行,即所有线程的引用关系不再变化。JVM 通过安全点(Safepoint)实现:

  • 安全点位置:方法调用、循环跳转、异常跳转等
  • 到达方式
    • 抢先式中断(已废弃):GC 时中断所有线程,不在安全点的恢复到安全点
    • 主动式中断(HotSpot 采用):设置一个标志位,线程在安全点主动检查并挂起
# 安全点相关参数
-XX:+PrintSafepointStatistics       # 打印安全点统计
-XX:PrintSafepointStatisticsCount=1 # 每次安全点都打印

安全区域(Safe Region)

安全点解决了”执行中”线程的问题,但对于阻塞或睡眠的线程(如 Thread.sleep()、等待 IO),它们无法主动到达安全点。

安全区域是安全点的扩展:在安全区域内,引用关系不会变化。线程进入安全区域时标记自己,离开时检查 GC 是否完成,若未完成则等待。

增量更新与原始快照(SATB)

在并发标记阶段,收集线程和用户线程同时运行,可能产生对象消失问题:

黑色对象(已标记)新引用了白色对象(未标记),同时灰色对象(未完全扫描)删除了对白色对象的引用。白色对象被错误回收。

两种解决方案:

增量更新(Incremental Update)

CMS 使用。当黑色对象插入对白色对象的新引用时,记录这个引用。GC 结束后,以这些记录的引用为根重新扫描。

本质:关注引用的新增。

原始快照(SATB, Snapshot At The Beginning)

G1 使用。当灰色对象删除对白色对象的引用时,记录被删除的引用。GC 结束后,以这些记录的引用为根重新扫描。

本质:关注引用的删除,保证 GC 开始时的对象图一致性。

            增量更新                     SATB
         记录新增引用                 记录删除引用
         (A → C 新增)              (B → C 删除)

   A(黑) ──新增──→ C(白)      A(黑) ──→ C(白)
   B(灰) ──删除──↗             B(灰) ──×──→ C

小结

本章介绍了垃圾回收的核心算法和理论基础。理解可达性分析和 GC Roots 是判断对象是否可回收的关键;理解三种基础算法的优缺点是后续学习具体收集器的基础;理解分代理论、记忆集和安全点有助于理解 GC 的实现机制。下一章将详细介绍各种垃圾收集器。

编辑此页

评论