强引用、软引用、弱引用、虚引用有什么区别?
简化版
四种引用回收强度依次减弱:强引用永不回收(宁可 OOM);软引用内存不足时才回收(适合缓存);弱引用下次 GC 就回收(如 WeakHashMap、ThreadLocalMap 的 key);虚引用几乎等于没引用,唯一作用是对象被回收时收到一个通知(管理堆外资源)。
详细版
| 引用类型 | 回收时机 | 典型用途 |
|---|---|---|
强引用 Object o = new Object() | 永不主动回收,宁可抛 OOM | 普通对象引用 |
软引用 SoftReference | 内存不足时回收 | 内存敏感的缓存 |
弱引用 WeakReference | 下次 GC 必回收(无视内存是否充足) | WeakHashMap、ThreadLocalMap key |
虚引用 PhantomReference | get() 恒为 null;对象进入虚可达处理后可被清除并入队 | 关联状态清理的兜底通知 |
- 强引用:最常见。只要有强引用指着,GC 绝不回收它,内存不够就 OOM。
- 软引用:
get()能拿到对象,但内存告急时 GC 会清掉它。适合做缓存——内存够就享受缓存加速,内存紧张就让路,不至于因缓存而 OOM。 - 弱引用:只要发生 GC 就被清掉,不管内存够不够。用于「对象活着我就用,对象没了也无所谓」的场景。
- 虚引用:无法通过它访问对象(
get()永远返回 null),唯一价值是配合ReferenceQueue,在收集器判定对象进入虚可达处理后接收入队通知,用于清理关联状态。
完整版教学
一、为什么需要「可回收的引用」
普通强引用有个问题:只要你还引用着,对象就永远不能回收。但有些场景,我们希望「引用它,但允许 JVM 在需要时把它回收掉」——比如缓存:我想缓存加速,但绝不希望缓存把内存撑爆导致 OOM。
强引用满足不了这个需求,于是 Java 提供了三种「弱化」的引用(软、弱、虚),把「是否回收」的决定权部分交还给 GC。这就是四种引用存在的意义:给对象的存活期提供不同强度的控制。
二、软引用 vs 弱引用:差在「内存是否充足」
这是最常被追问的对比:
- 软引用:收集器在响应内存需求时可以清理软可达对象,具体策略与时机由 JVM 决定。它表达“可以舍弃并重建”,不提供确定缓存策略。
- 弱引用:当对象只剩弱可达时,收集器在相应 GC 处理中会清除弱引用,与是否已经接近 OOM 无关。适合不应延长主体生命周期的附属映射。
SoftReference<byte[]> cache = new SoftReference<>(bigData);
// 内存充足时 cache.get() 拿得到 bigData;内存紧张时可能返回 null(已被回收)
WeakReference<Object> weak = new WeakReference<>(new Object());
System.gc(); // 只是回收建议,不保证立即执行
// 若后续 GC 已处理该弱引用,weak.get() 才会返回 null
WeakHashMap 就是用弱引用当 key:当某个 key 对象外部没有强引用并被 GC 处理后,映射会在后续访问等清理机会移除对应 entry——非常适合做「对象 → 附加数据」的映射,对象失去强可达性后,映射可在 GC 与后续 Map 操作中逐步清理。
三、虚引用到底干嘛用
虚引用最让人困惑:get() 永远返回 null,那要它何用?
它的价值不在「访问对象」,而在接收对象已进入虚可达处理阶段的通知。把虚引用关联到 ReferenceQueue 后,JVM 会按规范和实现规则清除、入队;清理线程可据此处理与引用对象绑定的外部状态。入队不提供精确时间承诺。
典型应用是 DirectByteBuffer:Java 对象在堆内,但它管理着一块堆外内存。堆外内存不属于 Java 堆,JDK 通过 Cleaner/虚引用相关机制提供兜底释放;业务持有的可关闭资源仍应显式关闭,不能等待不确定的 GC。
四、和 ThreadLocal 内存泄漏的联系
ThreadLocalMap 的 key 是弱引用的 ThreadLocal,value 是强引用。这个设计正是为了:当 ThreadLocal 外部强引用断了,key 能被 GC 回收,避免无用的 ThreadLocal 永久占位。但 value 仍是强引用,所以还是可能泄漏——这就是为什么 ThreadLocal 用完要 remove()(详见「ThreadLocal 的原理」那道题)。
在线程池中,工作线程可能存活数小时,ThreadLocalMap 的 value 因而会沿线程这条 GC Root 链长期保留。即使弱 key 已被清除,value 也要等 Map 后续操作触发清理或线程结束才有机会解除。因此可靠做法仍是在 finally 中调用 remove(),而不是等待弱引用机制兜底。
五、ReferenceQueue 才是引用处理的闭环
软、弱、虚引用都可关联 ReferenceQueue。当 JVM 按对应规则处理引用后,引用对象会在适当阶段入队,程序再清理与被引用对象关联的外部状态。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object target = new Object();
WeakReference<Object> ref = new WeakReference<>(target, queue);
target = null;
// GC 时机不确定;ref 入队后,消费者可清理配套元数据
虚引用的 get() 始终返回 null,主要用于接收对象到达特定回收阶段的通知。入队时间和 GC 本身都不确定,所以它不适合承担“必须在 100 ms 内关闭文件”这类确定性资源管理。
六、不同引用强度怎样选
| 引用类型 | 何时可能清理被引用对象 | 适合用途 | 不适合用途 |
|---|---|---|---|
| 强引用 | 不再强可达后 | 普通对象关系 | 可自动淘汰缓存 |
| 软引用 | 内存需求与实现策略触发 | 少量、可重建数据的辅助引用 | 有容量和命中率 SLA 的缓存 |
| 弱引用 | 只剩弱可达后的 GC 处理 | 不应延长 key 生命周期的映射 | 需要稳定保留的数据 |
| 虚引用 | 对象进入相应回收处理阶段 | 配合队列做事后通知 | 通过 get() 访问对象 |
假设缓存里有 100 万条、平均每条 2 KB,数据体约 2 GB,尚未包含 Map、引用对象和索引开销。把它们包成软引用不会创造容量上限,也无法提供确定的淘汰顺序;生产缓存通常更需要显式最大容量、过期策略和命中率指标。
Cleaner 内部使用虚引用类机制,可作为忘记关闭资源时的兜底,但正常路径仍应 try-with-resources。清理动作必须简短,且不能捕获目标对象形成强引用闭环。
引用类型控制的是“对象可达强度”,不是资源生命周期调度器,也不是缓存容量策略。
七、常见误区与追问
- 误区:软引用只要内存不够就一定按固定顺序回收。 具体清理策略由 JVM 实现和内存压力决定,没有业务级确定性。
- 误区:弱引用对象在赋值为 null 的瞬间立即消失。 它要等相应 GC 引用处理,发生时间不受业务代码精确控制。
- 误区:虚引用可以通过
get()重新取得对象。PhantomReference.get()始终返回null。 - 追问:为什么
WeakHashMap的 key 弱引用仍可能泄漏? value 若直接或间接强引用 key,可能重新形成从 Map 到 key 的强链。 - 追问:引用对象为什么要配队列? 队列让程序知道哪些引用已被处理,从而移除索引或清理关联的堆外状态。
- 追问:软引用适合做生产缓存吗? 通常不如具有容量、过期和统计策略的 Caffeine 等缓存可控。
- 追问:Cleaner 能替代
close()吗? 不能;它只适合兜底,执行时机不确定,资源应在正常控制流中及时释放。
八、加强记忆
记住四种强度的核心问题是“这条引用是否应该延长对象生命周期”:强引用正常保活,软引用允许在内存需求下清理,弱引用不应阻止主体回收,虚引用只配合队列接收处理通知。软弱引用的清理都依赖 GC,既不即时也没有业务级时间承诺;虚引用的 get() 始终为 null。ReferenceQueue 把引用处理与关联元数据清理连接起来,但不能代替确定性资源关闭。缓存需要容量、过期与命中率策略,资源需要 try-with-resources,不要把软引用或 Cleaner 当成完整生命周期方案。