什么是强引用、软引用、弱引用和虚引用?
结论先行:四种引用是从强到弱的可达性等级,决定了对象「什么时候可以被回收」:**强引用(strong reference)**永不主动回收,宁可 OOM;**软引用(soft reference)**只在内存不足时回收,适合做缓存;**弱引用(weak reference)**只要发生 GC 就回收,适合做非强制的关联;**虚引用(phantom reference)**不决定生命周期,只用于在对象被回收时收到通知,配合 ReferenceQueue 做资源清理。
一、四者对照
| 引用类型 | 回收时机 | 能否 get() 到对象 | 典型用途 |
|---|---|---|---|
| 强引用 | 永不主动回收,除非不可达 | 能 | 普通 new 出来的对象 |
| 软引用 | 内存不足、即将 OOM 前 | 未回收前能 | 图片/页面缓存 |
| 弱引用 | 下次 GC 时,无论内存是否充足 | 未回收前能 | ThreadLocal 的 key、WeakHashMap |
| 虚引用 | 随时可能被回收,get() 永远返回 null | 不能 | 回收通知、堆外内存释放 |
强 ─── 软 ─── 弱 ─── 虚
↑ ↑
最难回收 最易回收
强度越弱,越早被回收;GC Roots 可达性相同时,按引用强度决定去留。
二、软引用:内存敏感的缓存
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;
public class SoftCache {
private final Map<String, SoftReference<byte[]>> cache = new HashMap<>();
public byte[] get(String key, java.util.function.Supplier<byte[]> loader) {
SoftReference<byte[]> ref = cache.get(key);
byte[] value = ref == null ? null : ref.get();
if (value == null) { // 已被回收或从未加载
value = loader.get();
cache.put(key, new SoftReference<>(value));
}
return value;
}
public static void main(String[] args) {
SoftCache c = new SoftCache();
System.out.println(c.get("k1", () -> new byte[1024]).length); // 1024
}
}
软引用在「真正 OOM 之前」被清理,因此它能撑住缓存命中率而不至于直接把进程打死。缺点是回收策略不可控,GC 行为的微小变化就会导致缓存雪崩,线上缓存更推荐 Caffeine 这类带容量上限的实现。
三、弱引用:不阻止回收的关联
import java.lang.ref.WeakReference;
public class WeakDemo {
public static void main(String[] args) {
Object strong = new Object();
WeakReference<Object> weak = new WeakReference<>(strong);
System.out.println(weak.get() != null); // true,强引用还在
strong = null; // 断开强引用
System.gc();
System.out.println(weak.get() == null); // true,只剩弱引用,GC 后清空
}
}
WeakHashMap 是弱引用的经典应用:key 是弱引用,一旦外部不再持有 key,整个 entry 就可以回收,非常适合做「附加在某个对象上的元数据」。ThreadLocalMap 的 key 也是弱引用,但 value 是强引用,所以线程池里必须显式 remove(),否则 value 泄漏。
四、虚引用与清理机制
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public class PhantomDemo {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
byte[] resource = new byte[1024 * 1024];
PhantomReference<byte[]> phantom = new PhantomReference<>(resource, queue);
System.out.println(phantom.get()); // 永远 null
resource = null;
System.gc();
Reference<?> polled = queue.remove(2000); // 回收后入队,收到通知
System.out.println(polled != null ? "收到回收通知,可释放堆外资源" : "未收到");
}
}
DirectByteBuffer 的堆外内存释放就依赖这套机制:JDK 9 起用 Cleaner(内部组合了虚引用与 ReferenceQueue),在对象被回收时执行 Unsafe.freeMemory。
五、常见追问
| 追问 | 回答要点 |
|---|---|
| 软引用什么时候被清理 | 内存不足时;-XX:SoftRefLRUPolicyMSPerMB 控制「每 MB 空闲堆保留多久」,不建议随意调 |
| 弱引用会不会一创建就没了 | 不会,创建时若还有强引用就存活;只有强引用消失后,下一次 GC 才清理 |
虚引用为什么 get() 返回 null | 设计上就不允许通过虚引用访问对象,它只负责「回收后通知」 |
finalize 与四种引用的关系 | finalize 是在对象不可达后的一次自救机会;引用类型决定「是否可达」,两者层次不同 |
| 缓存到底该用哪种 | 弱引用适合「有则用、无则重建」;容量受限的业务缓存用 Caffeine/Guava,别手写软引用 |
小结
强度决定去留:强引用保命,软引用扛内存压力,弱引用随时可清,虚引用只做通知。记住各自「绑定」的场景,比背定义更有用。