写 Java 时我们每天都在 new 对象,但很少想过一个问题:这个对象什么时候会被 GC 回收? 答案取决于"谁在引用它,以及引用有多强"。JVM 把引用分为四个等级:强引用 > 软引用 > 弱引用 > 虚引用。下面逐个介绍,每讲完一种就给出对应的测试代码,跑一遍印象更深。
一、强引用(Strong Reference)
最普通的引用,Object obj = new Object() 这种直接赋值就是强引用。
回收时机 :只要强引用还在,GC 就永远不会回收这个对象------哪怕内存快要耗尽,JVM 宁可抛 OutOfMemoryError 也不回收。想回收它,只有一个办法:把引用置为 null 或者让引用离开作用域。
使用场景:日常代码 99% 都是强引用。正因为太常见,我们反而容易忽略它------Android 里最经典的内存泄漏就是 Activity 被 Handler、单例等强引用拽着不放导致的。
测试代码
csharp
/** 强引用:只有断开引用(置 null 或离开作用域),对象才能被 GC 回收 */
private static void strongTest() {
System.out.println("===== 强引用 =====");
Object obj = new Object(); // 普通的 new 赋值就是强引用
System.out.println("GC 前(强引用还在):" + obj);
obj = null; // 断开强引用,对象才有机会被回收
System.gc();
System.out.println("置 null 并 GC 后:" + obj);
}
输出:
kotlin
===== 强引用 =====
GC 前(强引用还在):java.lang.Object@1b6d3586
置 null 并 GC 后:null
变量 obj 本身是引用,置 null 后不再指向任何对象,原来的对象才能被回收。
原理
GC 判定对象是否存活,本质是看它是否被强引用链(GC Roots)可达。obj = null 切断了从栈到堆中对象的唯一引用链,对象变成不可达,所以 System.gc() 可以回收它。只要强引用链还在,对象即使再也没被业务代码用到,GC 也会把它当"活"的。
二、软引用(SoftReference)
用 SoftReference 包装对象:SoftReference<Object> softRef = new SoftReference<>(obj)。
回收时机 :内存充足 时,GC 不回收它;内存不足时,JVM 在抛出 OOM 之前会先把软引用对象清理掉。可以理解为一种"宁可断尾求生"的兜底策略。
使用场景:内存敏感的缓存。比如图片缓存------内存够用就留着加速访问,内存紧张就自动释放保命。Android 早期很多图片缓存方案就是软引用实现的(现在更推荐 LruCache,因为 LruCache 有明确的淘汰策略,可控性更强)。
注意:通过软引用取对象要用 get() 方法,对象被回收后 get() 返回 null,所以拿到后要做判空。
测试代码
csharp
/** 软引用:内存充足时 GC 不回收,内存不足时才回收 */
private static void softTest() {
System.out.println("===== 软引用 =====");
Object data = new byte[1024 * 1024]; // 1MB 的对象
SoftReference<Object> softRef = new SoftReference<>(data);
data = null; // 断开强引用,此时只剩软引用
System.gc(); // 内存充足,GC 不会动它
System.out.println("内存充足时 GC 后获取:" + softRef.get()); // 对象还在
// 疯狂申请内存,把堆撑满,模拟内存不足
try {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 每次 1MB,直到 OOM
}
} catch (OutOfMemoryError e) {
// JVM 抛 OOM 之前,已经先把软引用对象回收了
System.out.println("内存不足时获取:" + softRef.get()); // null
}
}
输出 (建议用 -Xmx64m 运行,OOM 触发更快):
kotlin
===== 软引用 =====
内存充足时 GC 后获取:[B@1540e19d
内存不足时获取:null
内存够的时候对象安然无恙;堆被撑满后,软引用对象会被 JVM 优先清理。
原理
JVM 在回收软引用时有个"余地":只有当堆剩余空间不足,或者即将 OOM 时,才会清理软引用。内存充足时执行 System.gc(),软引用对象仍然被 SoftReference 内部引用着,所以 get() 能拿到;堆被撑满后,JVM 不得不优先回收软引用对象来避免 OOM,此时 get() 返回 null。
三、弱引用(WeakReference)
用 WeakReference 包装对象:WeakReference<Object> weakRef = new WeakReference<>(obj)。
回收时机:比软引用更"命薄"------只要 GC 一发生,不管内存够不够,只有弱引用可达的对象立刻被回收。它活不过下一次 GC。
使用场景:
- WeakHashMap:key 是弱引用,外部不再使用该 key 时,entry 自动被清理,适合做"随用随弃"的关联数据。
- ThreadLocal :
ThreadLocalMap的 key 就是弱引用,防止 ThreadLocal 对象无法回收。 - Android 防内存泄漏 :用
WeakReference<Activity>或WeakReference<Context>持有引用,既能让内部类访问外部对象,又不妨碍 Activity 被回收。
测试代码
csharp
/** 弱引用:只要发生 GC,对象立刻被回收 */
private static void weakTest() {
System.out.println("===== 弱引用 =====");
Object data = new Object();
WeakReference<Object> weakRef = new WeakReference<>(data);
data = null; // 断开强引用,只剩弱引用
System.out.println("GC 前:" + weakRef.get());
System.gc();
System.out.println("GC 后:" + weakRef.get()); // null,弱引用活不过一次 GC
}
输出:
kotlin
===== 弱引用 =====
GC 前:java.lang.Object@4554617c
GC 后:null
System.gc() 一执行,只剩弱引用的对象就被回收了,这是和软引用最直观的区别。
原理
弱引用在 GC 的"可达性分析"阶段不会被当作有效的引用链。只要对象没有强引用或软引用可达,只有弱引用指向它,GC 就会直接把它标记为可回收。因此不管内存是否充足,System.gc() 后弱引用对象都会被回收,get() 自然变成 null。
四、虚引用(PhantomReference)
用 PhantomReference 包装对象,且创建时必须 传入一个 ReferenceQueue。
回收时机 :虚引用是"形同虚设"的引用------你永远无法通过它拿到对象,get() 永远返回 null。它唯一的作用是监听对象的死亡 :对象被 GC 回收后,虚引用会被塞进关联的 ReferenceQueue,程序通过轮询队列就能感知"某个对象刚刚死了"。
使用场景 :资源释放的"善后"工作。最典型的是 DirectByteBuffer------堆外内存不受 GC 管理,JVM 就是靠虚引用(Cleaner 机制)感知 Buffer 对象被回收,然后释放对应的堆外内存,防止内存泄漏。
测试代码
csharp
/** 虚引用:get() 永远返回 null,只能配合 ReferenceQueue 感知对象被回收的时机 */
private static void phantomTest() throws InterruptedException {
System.out.println("===== 虚引用 =====");
Object data = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(data, queue);
data = null;
System.out.println("GC 前 get():" + phantomRef.get()); // 永远是 null
System.gc();
Thread.sleep(500); // 稍等 GC 把虚引用塞进队列
Reference<?> ref = queue.poll(); // 从队列取出"已死亡对象"的虚引用
System.out.println("对象回收后,引用队列中拿到:" + ref);
System.out.println("是当初注册的那个虚引用吗?" + (ref == phantomRef));
}
输出:
csharp
===== 虚引用 =====
GC 前 get():null
对象回收后,引用队列中拿到:java.lang.ref.PhantomReference@74a14482
是当初注册的那个虚引用吗?true
拿不到对象本身,但对象被回收后,当初注册的虚引用会出现在引用队列里------这就是它的"报丧"机制。
原理
虚引用设计上就不允许通过它访问对象(get() 固定返回 null),这是为了避免你"复活"一个即将被回收的对象。它的价值在对象被回收的"后半段":GC 在回收对象前,会把关联的虚引用注册到 ReferenceQueue。业务线程通过 poll() 拿到这个信号后,就可以做清理工作,比如释放堆外内存、关闭文件句柄等。