# 垃圾回收机制前置知识

作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)


# 一、引用语义:谁在“指着”这个对象?

Java 通过引用强度影响对象的存活判定与回收时机。

# 1)强引用

只要存在强引用,GC 永远不会回收对象,即使内存紧张。

Object obj = new Object(); // 强引用
1

# 2)软引用

内存充足不回收,紧张时回收;常用于缓存。

SoftReference<Object> softRef = new SoftReference<>(new Object());
Object obj = softRef.get();
1
2

# 3)弱引用

发生 GC 就回收(不看内存是否紧张);常用于 WeakHashMap、ThreadLocal 的键等。

WeakReference<Object> weakRef = new WeakReference<>(new Object());
Object obj = weakRef.get();
1
2

# 4)虚引用

仅用于跟踪对象回收状态,不影响可达性。

ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
1
2

这些引用不会直接改变 GC 算法,但会在 可达性分析清理阶段 影响对象命运。


# 二、可达性分析:如何判断对象“还活着”

# 1)GC Roots 的来源

  • 线程栈帧中的局部变量表引用

  • 方法区(元空间)中的静态字段引用

  • 运行时常量池中的引用

  • 正在运行的线程对象本身

  • JNI(本地方法)引用

GC Roots 是堆外区域中的引用起点。

# 2)可达性分析算法

从 GC Roots 出发,沿着对象引用链递归查找:

  1. 标记阶段: 从 GC Roots 出发,把能沿引用链抵达的对象标记为“活”;

  2. 清理阶段: 没被标记的对象就是不可达对象,可以回收。

注意:可达 ≠ 正在使用;JVM 只以“从根是否可达”作为回收判定的客观标准。


# 三、对象标记机制与演进

# 1) 三色标记法


颜色 含义
白色 未被标记(可回收)
灰色 已发现但其引用未扫描完
黑色 已扫描完且所有引用对象均可达

执行流程:

  1. 所有对象初始为白色;

  2. GC Roots 标记为灰色;

  3. 不断扫描灰色对象,将其引用对象染灰,自身转为黑色;

  4. 扫描结束后,剩余白色对象为垃圾。

写屏障 & 记忆集(Card Table / RSet)

为解决并发标记时引用变更问题(如“黑指向白”),GC 利用写屏障和记忆集维护“三色不变式”:

AB // A 黑、B 灰
A.field = C; // 若 C 白,则需重新标灰
1
2

如果C是白色, A->C 违反了三色定义, 黑色的对象是可达并且所有子引用都扫描完, C有引用不应该被回收, 但是按照三色标记法会被回收。不同年代的垃圾回收器有不同的处理方式。

# 2) 跨代引用与写屏障机制

由于对象在年轻代与老年代间存在跨代引用,GC 引入以下机制优化扫描效率:

  1. 写屏障(Write Barrier)

    在引用写操作前后插入钩子,用于通知 GC “某个引用被修改了”。

    write_barrier(A, B) {
        record_reference_change(A, B);
        A.field = B;
    }
    
    1
    2
    3
    4
  2. Card Table(卡表)

    • 堆被划分为固定大小的 Card 区块;

    • 当老年代对象引用了年轻代对象,对应卡页被标记为脏;

    • Minor GC 时只扫描脏卡页,避免全堆扫描。

  3. RSet(Remembered Set)

    G1 收集器中每个 Region 自带一份“卡表”,记录其他 Region 指向本 Region 的引用; 避免全堆扫描,提高并行效率。

  4. CSet(Collection Set)

    G1 每次 GC 周期中选出的待回收 Region 集合:

    • 包含 Eden、部分 Survivor、部分 Old Region;
    • 基于“回收收益”模型优先清理垃圾比例高的 Region。

# 3) 收集器演进

  1. 早期阶段

    • 代表收集器

      Serial、ParNew、Parallel Scavenge

    • 全程 Stop-The-World

      垃圾回收时用户线程会暂停, 当完成可达性分析+回收工作后再恢复应用线程。

    • Minor GC 依赖卡表机制避免老年代全堆扫描, 解决跨代引用。

      OldObj.field = YoungObj;
      // 写屏障示意 只要某个老年代区域引用了年轻代,就把对应的卡页打脏。
      card_table[(address(OldObj) >> 9)] = DIRTY;
      
      1
      2
      3
  2. 并发标记阶段

    • 代表收集器

      CMS

    • 标记阶段与用户线程并发

    • Minor GC 依赖卡表机制避免老年代全堆扫描, 解决跨代引用。

    • 使用写后屏障防止“黑指向白”漏标问题;

      // 问题
      ABA 已标黑,B 已标灰)
      // 新增引用, 如果C为白色
      A.field = C; 
      
      // 解决方案 使用写屏障变更引用对象颜色
      if (A is black && C is white)
      	mark(C as gray) // 变更 mark bitmap 记录对象染色情况
      
      1
      2
      3
      4
      5
      6
      7
      8
  3. Region 化阶段

    • 代表收集器

      G1

    • 处理方式

      堆被划分为等大小 Region;使用 RSet 跟踪跨 Region 引用;通过 CSet 按收益优先选择回收目标。

# 四、核心回收算法

算法 过程 优点 缺点 典型应用
标记-清除 标记可达对象,清理未标记 实现简单 碎片多 老年代(基础实现)
标记-复制 复制存活对象到新区域 无碎片,快 空间浪费 新生代
标记-整理 标记后移动对象 消除碎片 需移动对象 老年代(Compact GC)
分代收集 按对象生命周期分代 效率高 实现复杂 各主流收集器

# 五、GC 触发条件

触发场景 类型
Eden 或 Survivor 区满 Minor GC
老年代空间不足 / 晋升失败 Major / Full GC
元空间压力或 System.gc() 调用 Full GC

# 六、YGC 与 Full GC 的扫描边界

# 1)YGC(Minor GC / Young GC)

  • 目标:只回收年轻代(Eden + Survivor)中的无用对象。

  • 根对象来源(Roots)

    • 线程栈、本地引用、静态字段
    • 老年代中被标记为 Dirty 的卡页(Card Table / RSet)
  • 回收流程: 从 Roots 开始扫描 → 标记年轻代存活对象 → 将其 复制 到 Survivor 区(或晋升到老年代)。

# 2)Full GC(Major GC)

  • 触发:老年代不足、晋升失败或显式 System.gc()
  • 范围:全堆(新生代 + 老年代 + 元空间)。
  • 并行收集器:全堆扫描;
  • G1 / ZGC:基于 Region / Page 的精确扫描,依赖 RSet 或染色指针识别引用。