这是《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 根扫描要求线程处于可识别引用位置的状态,即安全点。
常见安全点:
- 方法返回;
- 循环回边;
- 异常抛出;
- 特定调用位置。
问题:
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 后进入引用队列 |
注意:
- soft 行为与策略和内存压力相关;
- weak 不等于缓存容量上限;
- phantom 不应替代显式 close;
- ReferenceQueue 需要处理线程;
- 引用对象本身也要可达。
14.8 方法区与类回收
类卸载条件较苛刻:
- 类所有实例已回收;
- 加载它的 ClassLoader 已回收;
- Class 对象没有其他引用。
观察:
jcmd <pid> GC.class_histogram
jcmd <pid> VM.classloader_stats
类泄漏来源:
- 动态代理;
- 脚本引擎;
- 反射生成;
- 热部署;
- 容器隔离;
- 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 对象都释放。
思考题
- 为什么引用计数不适合 HotSpot?
- 常见 GC Roots 有哪些?
- 什么是浮动垃圾?
- 三色标记中黑色和灰色分别表示什么?
- 类卸载为什么依赖 ClassLoader 回收?