Object obj = new Object()这行代码太熟了,熟到大多数人从没想过:obj只是引用变量,而“引用”本身是分等级的。除了这种默认、最强硬的一种,JDK 还提供了软引用、弱引用、虚引用。它们改变的其实只是一件事——对象什么时候可以被回收。
这四种引用的回收门槛依次降低。用得对,是缓存和防内存泄漏的顺手工具;用错了,就是一堆“明明没引用它却还活着 / 明明在用却突然没了”的诡异 bug。下面从强到弱过一遍,重点讲清楚弱引用,以及它和软引用到底怎么选、怎么用。
一、四种引用,从强到弱
1. 强引用(Strong Reference)
就是我们平时写的new:
Object obj = new Object();只要这条强引用还在,GC 就绝不会回收它指向的对象。哪怕堆已经紧到马上要抛OutOfMemoryError,JVM 也更愿意自己 OOM,而不是动一个还有强引用的对象。这是默认、也是最强的一档。想让对象可回收,办法只有主动断开引用(obj = null)或者让它随作用域自然消失。
日常写业务,99% 的时间都只跟强引用打交道,另外三种是解决特定问题的工具。
2. 软引用(Soft Reference)
用SoftReference<T>包一层:
SoftReference<byte[]> cache = new SoftReference<>(new byte[1024 * 1024]); byte[] data = cache.get(); // 可能已经为 null规则是:内存够用时不动它,内存紧张时 GC 会把它清掉——而且清理发生在抛出 OOM 之前,相当于给堆留的最后一道缓冲。
这里有个细节大多数人不知道:软引用不是“内存一紧张就全清”。HotSpot 采用一种类似 LRU 的策略,由-XX:SoftRefLRUPolicyMSPerMB控制——每剩余 1MB 堆,软引用大约能“存活”1 秒(默认值 1000)。堆越空,软引用越容易被保留;堆越紧,越容易被清。这也是它天生适合做缓存的原因。
3. 弱引用(Weak Reference)
WeakReference<T>,本文的重点:
WeakReference<Object> ref = new WeakReference<>(new Object()); Object o = ref.get(); // 只要发生过一次 GC,这里大概率是 null它比软引用还弱:只要对象只被弱引用指着(没有强引用链能到达它),那么下一次 GC 就会回收它,跟内存够不够完全无关。换句话说,弱引用根本挡不住 GC。
有个容易踩的误区值得点破:弱引用并不改变对象“是否可达”的判定。如果对象还被别的强引用持有,它照样活着,弱引用不会让它提前被回收;弱引用只是在没人强引用它之后,不构成“活下去”的理由。
典型用法是WeakHashMap、监听器的注册表,以及“对象活着我才缓存、对象死了缓存自动失效”这类场景。
4. 虚引用(Phantom Reference)
PhantomReference<T>,最特殊的一个:
ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);两个特征:一是get()永远返回 null,你没法通过它拿到对象;二是它必须和ReferenceQueue配合使用。它唯一的意义在于:当对象即将被回收时,JVM 会把对应的虚引用塞进队列,程序据此得知“某个对象没了”,从而做清理。典型用途是管理堆外内存——DirectByteBuffer的Cleaner就是这么实现的。
注意它是“回收之后的通知”,而不是“阻止回收”。
二、四种引用对照表
| 引用类型 | 回收时机 | get() 结果 | 典型用途 |
|---|---|---|---|
| 强引用 | 只要引用链可达就不回收 | 对象本身 | 普通对象 |
| 软引用 | 内存不足时回收(OOM 之前) | 存在则返回,否则 null | 内存敏感缓存 |
| 弱引用 | 下次 GC 即回收 | 存在则返回,否则 null | WeakHashMap、监听器 |
| 虚引用 | 对象被回收后进队列通知 | 恒为 null | 堆外内存清理、回收跟踪 |
把这张表记住,选型基本就不会错:要不要留住对象,看它的“门槛”在哪一档。
三、弱引用的典型使用场景
1. WeakHashMap 做缓存。键是弱引用,当键对象不再被其他地方强引用时,这个键值对会被自动移除,从而避免缓存把键“焊死”在内存里:
WeakHashMap<Key, Value> cache = new WeakHashMap<>(); Key key = new Key(); cache.put(key, new Value()); key = null; // 唯一强引用消失,下次 GC 后该条目会被清掉不过这里有个反直觉的坑:WeakHashMap 只弱化“键”,值仍然是强引用。如果值反过来强引用了键(比如 value 内部持有 key 的引用),就构成了“键 → 值 → 键”的强引用闭环,键永远回收不掉,条目也就一直清不掉。它被误当成通用缓存、最后翻车,多半栽在这儿。
2. 监听器 / 回调注册表。组件注册监听器时,如果容器用强引用持有监听器,而监听器又持有宿主,宿主就永远释放不掉。用弱引用持有监听器,对象销毁后条目自动失效,省掉手动反注册:
class Listener { private final WeakReference<Target> targetRef; public Listener(Target target) { this.targetRef = new WeakReference<>(target); } }3. 打破双向关联里的“反拖”。父子双向关联是内存泄漏重灾区:父持有子、子又持有父。把其中反向的那一侧(子 → 父)改成弱引用,就能避免相互拖累。
class Parent { private final List<Child> children = new ArrayList<>(); } class Child { private WeakReference<Parent> parent; // 反向引用用弱引用 }这里要澄清一个常见误解:Java 的 GC 靠可达性分析,本来就能回收“循环引用”,所以弱引用在这里解决的不是“GC 收不掉循环”,而是——当外部只引用子对象时,子对象不该反过来把父对象一直拽着、导致父对象无法释放。
四、弱引用还是软引用,怎么选
两者最容易混。一句话区分:软引用赌的是“内存还够”,弱引用赌的是“还有人用”。
- 想拿它做缓存、又愿意在内存紧张时让出:用软引用。它给了内存压力一个缓冲,堆空时保留得住。
- 想让它“随对象的生命周期自动失效”、不参与保命:用弱引用。比如缓存本身不该成为对象存活的原因。
- 但软引用也有坑:它可能被 JVM 在某个不稳定时刻集中清理,导致缓存命中率抖动;而弱引用在每次 GC 都可能丢掉,过于激进。真正要做高性能缓存,
Caffeine、Guava Cache这类带淘汰策略的组件,比裸用引用类型靠谱得多。
五、总结
回到开头:这四种引用区分的不是“能不能访问对象”,而是“对象什么时候可以被回收”,门槛从强到弱依次是——只要可达就不收(强)、内存不足才收(软)、下次 GC 就收(弱)、回收后给你个通知(虚)。
带走几条:
- 强引用是默认行为,也是内存泄漏的常见源头之一,别让长生命周期容器无意中强持短生命周期对象;
- 软引用适合内存敏感缓存,但它“活得久不久”由堆的余量决定,别指望它精确保留;
- 弱引用不挡 GC,只解决“别让它成为保命理由”,
WeakHashMap、监听器、双向关联的反向引用是它的主场; - 虚引用
get()恒为 null,离开ReferenceQueue就没有意义,它的舞台是堆外内存这类“必须知道对象何时消失”的清理工作。
最后一句实话:WeakReference、SoftReference这些工具很锋利,但绝大多数业务代码压根用不上它们。真正会用,是在你明确知道“谁该决定这个对象的生死”的时候——而不是为了显得懂 JVM 而在代码里塞几个引用类。