跳转至

GC Roots 与三色标记法

GC Roots 是可达性分析的起点,三色标记法是现代低延迟 GC 的核心标记算法,通过写屏障解决并发时的漏标问题。


GC Roots

GC Root 是垃圾回收的**起点**。从这些对象出发,能走到的就是活的,走不到的就是垃圾。

七种 GC Root

类型 说明 示例
虚拟机栈中的引用 每个线程栈帧中的局部变量 方法里的 User user = new User()
静态变量 类的 static 引用 private static AppConfig instance
常量池引用 static final 引用的对象 private static final String NAME
JNI 引用 本地方法中持有的 Java 对象 native void method(Buffer buf)
活跃线程 每个正在运行的线程 Thread 对象
同步锁 被 synchronized 持有的对象 synchronized (sharedLock)
Class 对象 已加载类对应的 Class 对象 User.class

可达性分析图

graph TD
    R1[虚拟机栈局部变量] --> A[对象 A]
    A --> B[对象 B]
    B --> C[对象 C]
    B --> D[对象 D]

    R2[静态变量] --> E[对象 E]
    E --> F[对象 F]

    R3[JNI 引用] --> G[对象 G]

    R4[活跃线程] --> H[对象 H]
    R5[同步锁] --> I[对象 I]
    R6[Class 对象] --> J[对象 J]

    K[对象 K] -.->|无引用链| R1
    L[对象 L] -.->|仅被 K 引用| K

    style K fill:#ff6b6b,color:#fff
    style L fill:#ff6b6b,color:#fff
    style R1 fill:#51cf66,color:#fff
    style R2 fill:#51cf66,color:#fff
    style R3 fill:#51cf66,color:#fff
    style R4 fill:#51cf66,color:#fff
    style R5 fill:#51cf66,color:#fff
    style R6 fill:#51cf66,color:#fff

K 和 L 没有任何 GC Root 能到达 → 垃圾,可回收。


三色标记法

垃圾回收中用来标记对象存活状态的算法,是 G1、ZGC、Go 等现代 GC 的核心。

三种颜色

颜色 含义 最终状态
⚪ 白色 还没扫描到的 垃圾,可回收
🔘 灰色 扫描过了,但它引用的对象还没扫 等待继续扫描
⚫ 黑色 扫描过了,它引用的对象也都扫了 存活

完整标记过程

用一个例子演示:

对象关系:
GC Roots → A → B → C → G
              ↓     ↓
              D     (G 仅被 C 引用)
GC Roots → E → F

初始状态:GC Root 直接引用的对象标灰,其余全白。

⚫ A    ⚪ B    ⚪ C    ⚪ D    ⚫ E    ⚪ F    ⚪ G

扫描 A:A 有引用 → B,A 变黑,B 变灰。

⚫ A    🔘 B    ⚪ C    ⚪ D    ⚫ E    ⚪ F    ⚪ G

扫描 B:B 有引用 → C、D,B 变黑,C 和 D 变灰。

⚫ A    ⚫ B    🔘 C    🔘 D    ⚫ E    ⚪ F    ⚪ G

扫描 C:C 有引用 → G,C 变黑,G 变灰。

⚫ A    ⚫ B    ⚫ C    🔘 D    ⚫ E    ⚪ F    🔘 G

扫描 D、F、G:无更多引用,全部变黑。

⚫ A    ⚫ B    ⚫ C    ⚫ D    ⚫ E    ⚫ F    ⚫ G

所有对象变黑,标记完成,无白色对象可回收。

graph LR
    subgraph "① 初始状态"
        A1[⚫A] --> B1[⚪B]
        E1[⚫E] --> F1[⚪F]
    end
    subgraph "② 扫描 A"
        A2[⚫A] --> B2[🔘B]
        E2[⚫E] --> F2[⚪F]
    end
    subgraph "③ 扫描 B"
        A3[⚫A] --> B3[⚫B]
        B3 --> C3[🔘C]
        B3 --> D3[🔘D]
        E3[⚫E] --> F3[⚪F]
    end

流程总结

graph TD
    A[GC Root 标灰] --> B[取一个灰色对象]
    B --> C[扫描它的引用]
    C --> D[灰变黑,引用的白变灰]
    D --> E{还有灰色?}
    E -->|是| B
    E -->|否| F[剩余白色 = 垃圾]

并发标记的两大问题

三色标记通常和业务线程**并发执行**,会产生问题:

问题一:漏标 — 活对象被误判为垃圾 💀

**同时**满足两个条件才会漏标:

  1. 存在黑色对象到白色对象的**新引用**
  2. 所有灰色对象到该白色对象的引用都被**删除**
sequenceDiagram
    participant GC as GC 线程
    participant App as 业务线程

    Note over GC: ⚫A 🔘B ⚪C<br>A→C, B→C
    App->>App: A 删除 A→C
    App->>App: B 新增 B→C
    Note over GC: A 是黑色,不会再被扫描<br>C 是白色 → 误判为垃圾!💥

问题二:多标 — 死对象被当成活的(浮动垃圾)😅

并发期间某个对象变成垃圾,但标记阶段已经过了它 → 这次回收不了。下次 GC 就能回收,不致命,只是多占一轮内存。

解决漏标

方案 策略 使用者 优缺点
增量更新 黑色新增引用时,把它变回灰色,重新扫描 CMS 缺点:Remark 阶段可能较长
SATB(原始快照) 删除引用时,记录被删除的对象,标记它 G1 缺点:可能多标浮动垃圾,但不会漏标
染色指针 + 读屏障 硬件级别解决 ZGC 暂停 <1ms

对比总结

传统标记 三色标记
状态 二元(已访问/未访问) 三态(白/灰/黑)
并发能力 无法区分"还没扫到"和"确认是垃圾" 精确追踪进度,写屏障维护一致性
使用场景 单线程 GC 并发 GC(G1、ZGC、Go)

练习题

题目一:为什么多标(浮动垃圾)不致命,但漏标会导致程序崩溃?
答案

多标只是把本该回收的对象多留了一轮,下次 GC 就能回收,影响只是多占一点内存。漏标则相反——活对象被当成垃圾回收了,程序还在用这个对象但内存已经被释放,导致空指针、数据损坏甚至 JVM 崩溃。

题目二:G1 的 SATB 和 CMS 的增量更新有什么区别?
答案

增量更新在"新增引用"时拦截:黑色对象新增了对白色对象的引用,就把黑色变灰重新扫描。SATB 在"删除引用"时拦截:灰色对象删除了对白色对象的引用,就把被删除的白色对象记录下来,在标记结束时强制标灰。G1 选 SATB 是因为删除引用更常见(对象字段赋 null),拦截成本更低。

相关链接