JVMNotes

第 14 章:判定对象存活

zjc 于 2026-01-14 发布

这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 GC 需要判断对象是否可达。主流 HotSpot 使用可达性分析,而不是引用计数。并发标记阶段还会遇到三色标注、浮动垃圾和漏标风险。

14.1 引用计数

object.references++
object.references--
references == 0 -> recycle

优点是实现直观、判定快,但存在循环引用问题:

class Node {
    Node next;
}

Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null;
b = null;

即使 a 和 b 已不可达,两者计数仍不为 0。HotSpot 主流实现不采用引用计数判定对象存活。

14.2 可达性分析

从 GC Roots 出发:

GC Roots
  |-- thread stacks
  |-- static fields
  |-- JNI references
  |-- class loaders
  +-- monitor locks

遍历对象图
  reachable  -> survive
  unreachable -> candidate

常见根:

说明
线程栈 局部变量和操作数栈
静态字段 类持有的引用
常量引用 常量池中的引用
JNI native 全局和局部引用
系统类 核心类加载器对象
同步监视器 被锁对象

14.3 安全点

GC 根扫描要求线程处于可识别引用位置的状态,即安全点。

常见安全点:

  1. 方法返回;
  2. 循环回边;
  3. 异常抛出;
  4. 特定调用位置。

问题:

GC 想停顿
  -> 某线程长时间运行到安全点
     -> 其他线程等待
        -> 停顿时间变长

排查诊断参数:

-XX:+PrintSafepointStatistics
-XX:+PrintGCApplicationStoppedTime

参数可用性和输出格式与 JDK 版本相关。

14.4 三色标记

三色抽象:

颜色 含义
未访问
自身已访问,引用字段未处理完
自身和引用字段已处理

流程:

roots -> gray set
while gray not empty
  object = pop gray
  scan fields
  referenced white -> gray
  object -> black

并发标记时应用线程同时在修改引用,因此必须保证不漏标存活对象。

14.5 多标与少标

多标:

对象标记时可达
  -> 标记后立刻不可达
     -> 成为浮动垃圾

浮动垃圾通常下一轮标记回收。

少标更严重:

存活对象未标记
  -> 被错误回收
     -> 程序数据损坏

并发收集器通过写屏障、快照和读屏障等机制保证不漏标。

14.6 并发标记阶段

G1 典型并发标记周期:

Initial Mark
  -> Root Scan
     -> Concurrent Mark
        -> Remark / Final Mark
           -> Cleanup
阶段 是否 STW
Initial Mark 是,通常较短
Concurrent Mark
Remark
Cleanup 部分停顿

ZGC、Shenandoah 的阶段和屏障设计不同,不能直接套用 G1 的阶段图。

14.7 引用对象

SoftReference<Cache> soft;
WeakReference<Meta> weak;
PhantomReference<Resource> phantom;

可达性级别:

类型 回收行为
strong 可达即存活
soft 内存不足时倾向回收
weak 下次 GC 发现仅弱可达即回收
phantom 对象 finalize 后进入引用队列

注意:

  1. soft 行为与策略和内存压力相关;
  2. weak 不等于缓存容量上限;
  3. phantom 不应替代显式 close;
  4. ReferenceQueue 需要处理线程;
  5. 引用对象本身也要可达。

14.8 方法区与类回收

类卸载条件较苛刻:

  1. 类所有实例已回收;
  2. 加载它的 ClassLoader 已回收;
  3. Class 对象没有其他引用。

观察:

jcmd <pid> GC.class_histogram
jcmd <pid> VM.classloader_stats

类泄漏来源:

  1. 动态代理;
  2. 脚本引擎;
  3. 反射生成;
  4. 热部署;
  5. 容器隔离;
  6. Agent。

14.9 MAT 判定泄漏

步骤:

打开 hprof
  -> Leak Suspects
     -> Dominator Tree
        -> Top Consumers
           -> Path to GC Roots
              -> 找业务引用链
视图 用途
Histogram 对象数量
Dominator Tree 谁持有内存
Path to GC Roots 为什么不可回收
Duplicate Classes 类加载器冲突
Thread Overview 线程引用

过滤 weak / soft references 可以突出强引用路径。

14.10 常见误判

误判 事实
引用为 null 立即回收 只是不可达候选
GC 后旧对象一定回收 可能是浮动垃圾
weak cache 有容量上限 没有
heap dump 无影响 通常会触发 GC 和停顿
静态字段一定泄漏 只要有意管理就不是

生产 dump 包含敏感数据,必须有审批、脱敏、加密和保存期限。

本章小结

HotSpot 用可达性分析判断对象存活,从 GC Roots 遍历对象图。并发标记需要处理三色标记、浮动垃圾和漏标风险。引用强度影响回收倾向,但 soft / weak 不是缓存容量治理。类回收依赖实例、ClassLoader 和 Class 对象都释放。

思考题

  1. 为什么引用计数不适合 HotSpot?
  2. 常见 GC Roots 有哪些?
  3. 什么是浮动垃圾?
  4. 三色标记中黑色和灰色分别表示什么?
  5. 类卸载为什么依赖 ClassLoader 回收?