Java 引用类型决定了对象在内存中的”存活边界”:除了默认的强引用,JVM 还提供软引用、弱引用、虚引用三种特殊引用,让我们能在缓存、内存敏感场景与堆外资源回收中精细控制对象的生命周期。理解强引用、软引用、弱引用、虚引用的回收时机,是写出不内存泄漏、不 OOM 的 Java 代码的基础。
一、为什么需要四种引用
在 Java 世界里,绝大多数对象都通过普通的赋值语句创建,这种默认方式就是强引用。强引用的特点是:只要引用链还可达,垃圾回收器就绝不会回收它。问题在于,有些对象我们希望”内存够用就留着、不够用就扔掉”,或者”只要没人用了就该回收,别因为我的缓存而泄漏”——强引用做不到这种弹性。
为此,JDK 在 java.lang.ref 包里提供了 SoftReference、WeakReference、PhantomReference 三种引用包装类,配合 ReferenceQueue,把”对象什么时候可以被回收”这件事交还给开发者控制。下面逐一拆解它们的回收时机与典型用法。
二、强引用(Strong Reference):最熟悉的默认引用
日常写的每一句 Object o = new Object() 都是强引用。只要栈上或静态区还存在指向它的强引用,GC 就视其为”绝对不可回收”。
public class StrongRefDemo {
public static void main(String[] args) {
// 强引用:obj 指向的对象永远不会被 GC 回收
Object obj = new Object();
System.out.println(obj);
// 唯一让强引用对象可回收的办法:断开引用
obj = null;
System.gc(); // 此时对象才有资格被回收
}
}
内存泄漏的本质,往往就是”本该断开的强引用忘了置 null”——比如把对象塞进一个长期存活的 List 或 static Map 后不再清理。关于堆结构与回收参数的底层原理,可以回顾 JVM 内存模型与 GC 调优 这篇。
三、软引用(SoftReference):内存不足才回收,缓存利器
软引用用 SoftReference 包裹对象:只要堆内存还充足,GC 就不会回收它;只有当内存即将不足、即将抛出 OOM 之前,JVM 才会批量回收软引用对象(按最近最少使用顺序)。这使它天然适合做”内存敏感型本地缓存”。
public class SoftCache {
private SoftReference<byte[]> cache;
public byte[] get() {
byte[] data = (cache != null) ? cache.get() : null;
if (data == null) {
data = loadBigFile(); // 已被回收,重新加载
cache = new SoftReference<>(data);
}
return data;
}
private byte[] loadBigFile() { /* 读取大文件或计算结果 */ return new byte[0]; }
}
软引用的回收是”尽力而为”的,所以它适合兜底缓存:用本地内存加速,但绝不会因缓存撑爆而 OOM。如果你的缓存需要跨进程共享、持久化或带过期策略,那就应该上 Redis 这类分布式缓存,可参考 Redis 缓存与分布式锁实战 来取舍本地软引用与远程缓存的边界。
四、弱引用(WeakReference):下次 GC 必回收,防泄漏首选
弱引用比软引用更”弱”:无论内存是否充足,只要发生一次 GC,弱引用指向的对象就会被回收。这一特性常被用来做”附属映射”—— key 只被弱引用持有,key 本身没人用了,整条记录就该消失。
public class WeakMapDemo {
public static void main(String[] args) {
WeakHashMap<Key, Value> map = new WeakHashMap<>();
Key k = new Key("k1");
map.put(k, new Value("v1"));
k = null; // 断开外部强引用
System.gc(); // 下一次 GC 后,k 对应的 entry 被自动清理
System.out.println(map.size()); // 大概率输出 0
}
}
经典实战是 ThreadLocal:它的内部 ThreadLocalMap 用弱引用持有 key(即 ThreadLocal 本身),但 value 仍是强引用。如果线程是线程池里的长生命周期线程,且忘了调用 remove(),value 就会一直挂着——这正是 ThreadLocal 内存泄漏的根因。所以用完 ThreadLocal 一定要 remove()。
五、虚引用(PhantomReference):仅用于跟踪回收
虚引用是最”虚”的一种:它永远无法通过 get() 拿到对象(始终返回 null),并且必须配合 ReferenceQueue 使用。它的唯一用途,是在对象被回收时收到一个通知,从而执行清理动作——典型场景是释放堆外内存(如 Netty 的 DirectBuffer、Unsafe 分配的直接内存)。
public class PhantomCleanup {
public static void main(String[] args) {
ReferenceQueue<Resource> queue = new ReferenceQueue<>();
Resource res = new Resource();
// 虚引用的 get() 永远返回 null,只能靠队列感知回收
PhantomReference<Resource> phantom = new PhantomReference<>(res, queue);
res = null;
System.gc();
// 轮询引用队列,对象被回收后引用对象会入队
Reference<? extends Resource> ref = queue.poll();
if (ref != null) {
// 此时可安全释放该对象关联的堆外资源
cleanup(ref);
}
}
private static void cleanup(Reference<?> ref) { /* 释放直接内存等 */ }
}
框架底层大量借助引用机制做”魔法”,例如 Spring 等容器通过 Java 反射实战 拿到字段后,用弱引用保存代理对象缓存,避免 Proxy 类把目标对象永久钉在内存里。
六、引用队列 ReferenceQueue 的统一机制
软、弱、虚引用都可以挂到一个 ReferenceQueue 上。当它们的 referent 被 GC 判定为可回收时,引用对象本身(不是 referent)会被放入队列。开发者通过轮询队列,就能在对象”死后”执行善后逻辑,例如清理关联的本地缓存、关闭文件句柄或释放堆外内存。
ReferenceQueue<Resource> queue = new ReferenceQueue<>();
// 注册多个软/弱/虚引用到同一队列
SoftReference<Resource> r1 = new SoftReference<>(resA, queue);
WeakReference<Resource> r2 = new WeakReference<>(resB, queue);
// 后台线程统一处理被回收的引用
Reference<? extends Resource> ref;
while ((ref = queue.poll()) != null) {
cleanup(ref); // 根据 ref 执行对应清理
}
七、四种引用对比与选型
| 引用类型 | 回收时机 | get() 能否取值 | 典型用途 |
|---|---|---|---|
| 强引用 | 永不(除非显式置 null) | 能 | 普通对象、业务数据 |
| 软引用 | 内存不足 / OOM 前 | 能 | 内存敏感本地缓存 |
| 弱引用 | 下次 GC 必回收 | 能 | 防泄漏映射(WeakHashMap、ThreadLocal) |
| 虚引用 | 随时(仅跟踪) | 不能(恒 null) | 堆外资源回收跟踪 |
一句话选型:默认用强引用;想在内存紧张时自动让出空间的本地缓存用软引用;想让”没人用的 key”自动消失的附属映射用弱引用;想在对象销毁时收到通知去清理外部资源用虚引用。
八、常见坑与排障清单
- 把软/弱引用当”永久缓存”:软引用在内存充足时也会长期存活,弱引用则下次 GC 就没了,二者都不能替代有 TTL 的 Redis 缓存。
- WeakHashMap 的 key 必须无强引用:如果外部还用变量指着 key,entry 就清不掉,等于退化成普通 Map。
- 忘记清理 ThreadLocal:线程池线程 + 不 remove = value 强引用泄漏,长周期服务尤其要警惕。
- 虚引用误用 get():它永远返回 null,取对象只能靠引用队列里的通知,别试图从引用对象里拿回实例。
- ReferenceQueue 不消费会堆积:引用对象入队后若长期不 poll,它们本身也会占用内存,清理线程要保证执行。
九、总结
Java 引用类型把”对象何时可被回收”的控制权交到了开发者手里:强引用保命、软引用兜底缓存、弱引用防泄漏、虚引用做回收跟踪。掌握四者的回收时机与 ReferenceQueue 的配合,你就能在缓存设计、内存优化和堆外资源管理上少踩一大半坑。下一次遇到诡异的内存泄漏或 OOM,不妨先问一句:是不是该用弱引用,而不是死死攥着强引用不放?




