可达性分析与引用类型
垃圾回收的第一步是判断对象是否还活着。JVM 主要通过可达性分析完成这件事。
# 1. 引用计数的问题
引用计数思路很简单:对象被引用一次,计数加一;引用失效,计数减一。计数为零就可以回收。
问题是无法很好处理循环引用。
class Node {
Node next;
}
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null;
b = null;
a 和 b 互相引用,但外部已经无法访问它们。如果只看引用计数,它们的计数不为零,可能无法回收。
# 2. 可达性分析
可达性分析从 GC Roots 出发,沿引用关系向外搜索。
GC Roots -> 对象A -> 对象B -> 对象C
能被搜索到的对象是可达对象,不能被搜索到的对象可以被回收。
这种方式可以解决循环引用问题。
# 3. 常见 GC Roots
常见 GC Roots 包括:
- 栈帧局部变量表中的引用。
- 方法区中的静态变量引用。
- 方法区中的常量引用。
- JNI 本地引用。
- 活跃线程。
- JVM 内部对象。
- 被同步锁持有的对象。
排查内存泄漏时,核心就是找到对象为什么还能从 GC Roots 到达。
# 4. 强引用
强引用是最常见引用。
User user = new User();
只要强引用还存在,对象就不会被 GC 回收。
集合、缓存、静态变量中的引用如果没有清理,最容易造成内存泄漏。
# 5. 软引用
软引用用 SoftReference 表示。
SoftReference<byte[]> ref = new SoftReference<>(new byte[1024]);
软引用对象在内存不足时可能被回收,曾经常用于缓存。但现代缓存更推荐使用成熟缓存框架,因为软引用回收时机不够可控。
# 6. 弱引用
弱引用用 WeakReference 表示。
WeakReference<User> ref = new WeakReference<>(new User());
只要发生 GC,弱引用关联对象如果没有强引用,就可能被回收。
典型应用:
WeakHashMap。- ThreadLocal 的 key。
- 一些监听器和元数据缓存。
# 7. 虚引用
虚引用用 PhantomReference 表示。
它不能通过 get() 获取对象,主要用于对象被回收时接收通知,配合引用队列管理堆外资源。
直接内存释放相关机制就和虚引用、Cleaner 思想有关。
# 8. finalization
对象真正回收前可能涉及 finalize,但 finalize 已经过时,不推荐使用。
原因:
- 执行时机不可控。
- 性能差。
- 容易导致对象复活。
- 可能造成资源释放延迟。
资源管理应使用 try-with-resources、显式 close 或 Cleaner 等更可控机制。
# 9. 阶段小结
可达性分析回答“对象为什么还活着”。引用类型回答“引用有多强”。排查内存泄漏时,最重要的是沿 GC Roots 引用链找到强引用来源,而不是只看对象本身占了多少内存。
# 10. 可达性分析图解
GC Roots
├─ 栈中局部变量 refA ──► Object A ──► Object B
├─ 静态变量 cache ───► Object C ──► Object D
└─ JNI 引用 ───► Object E
Object X ──► Object Y
▲
└── 只有互相引用,但没有从 GC Roots 可达
在这个图里:
- A、B、C、D、E 都可达,不能回收。
- X、Y 虽然互相引用,但不可从 GC Roots 到达,可以回收。
# 11. 四种引用对比表
| 引用类型 | 回收行为 | 典型用途 | 注意点 |
|---|---|---|---|
| 强引用 | 只要可达就不回收 | 普通对象引用 | 最容易造成泄漏 |
| 软引用 | 内存不足时可能回收 | 历史上用于缓存 | 回收时机不可控 |
| 弱引用 | 下次 GC 即可回收 | WeakHashMap、ThreadLocal key | value 仍可能泄漏 |
| 虚引用 | 无法直接获取对象 | 回收通知、资源清理 | 必须配合 ReferenceQueue |
# 12. ThreadLocal 为什么容易泄漏
ThreadLocalMap 的 key 是弱引用,但 value 是强引用。
Thread
└─ ThreadLocalMap
└─ Entry
├─ key: WeakReference<ThreadLocal>
└─ value: Object strong reference
如果 ThreadLocal key 被回收,但线程长期存活,value 可能仍被 Entry 持有,造成泄漏。
正确写法:
try {
threadLocal.set(value);
// business
} finally {
threadLocal.remove();
}
# 13. Tips 快问快答
Q:循环引用一定不能回收吗?
A:能回收。只要循环中的对象不可从 GC Roots 到达,就可以被可达性分析识别为垃圾。
Q:弱引用对象什么时候回收?
A:当对象只被弱引用关联时,下一次 GC 就可能回收。
Q:软引用适合做缓存吗?
A:现代业务更推荐 Caffeine 这类明确容量和淘汰策略的缓存,软引用缓存不够可控。
Q:内存泄漏分析为什么看 GC Roots?
A:因为泄漏对象之所以不被回收,一定存在从 GC Roots 到它的引用链。