强引用、软引用、弱引用、虚引用:从 Java 对象生命周期到内存优化,一篇讲透
面试官:“Java 有哪几种引用类型?分别有什么特点?”
你:“强引用是永不回收,OOM 也不回收;软引用在内存不足时回收;弱引用下一次 GC 必回收;虚引用主要用于管理堆外内存,必须配合引用队列。”
面试官:“那你用过软引用或弱引用吗?WeakHashMap 是怎么工作的?”
你:“……”
很多人能背出四种引用的回收时机,但一追问“为什么需要这么多引用类型”“如何利用它们优化内存”就含糊了。本文从 GC 可达性角度,结合代码示例,彻底讲透 Java 引用体系。
一、为什么需要区分引用类型?
Java 垃圾回收(GC)的基本思想是回收“死亡”对象,而对象的存活与否取决于可达性。传统的强引用(Object obj = new Object())会导致对象永远可达,除非引用被主动置空。但在某些场景下,我们希望能更灵活地控制对象的生命周期:
- 缓存:希望缓存的对象在内存充足时保留,内存紧张时自动释放。
- 避免内存泄漏:某些容器类持有对象引用,导致对象无法被回收。
- 堆外内存管理:需要感知对象被回收,以便清理堆外资源。
因此,Java 在 1.2 版本引入了软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference),再加上默认的强引用(StrongReference),共同构成了完整的引用体系。
二、可达性强度与引用类型
GC 在判断对象是否可回收时,会沿着引用链从 GC Roots 出发,根据引用的强度进行不同处理。从强到弱依次为:
| 可达性级别 | 含义 | 回收时机 |
|---|---|---|
| 强可达 | 存在强引用链 | 永不回收(除非引用消失) |
| 软可达 | 存在软引用,没有强引用 | 内存不足时回收 |
| 弱可达 | 存在弱引用,没有强/软引用 | 下一次 GC 即回收 |
| 虚可达 | 存在虚引用,没有其他引用 | 随时可能回收,无法通过虚引用获取对象 |
| 不可达 | 无任何引用 | 肯定回收 |
三、强引用(Strong Reference)
1. 定义
强引用是最常见的引用方式,例如Object obj = new Object()。只要强引用存在,GC 就永远不会回收这个对象,即使抛出OutOfMemoryError也不会回收。
2. 特点
- 默认的引用类型。
- 显式置为
null才会断开引用,帮助 GC 回收。 - 强引用导致内存泄漏的常见原因:长生命周期容器(如
static集合)持有短生命周期对象的引用。
3. 示例
ObjectstrongRef=newObject();// 强引用System.gc();// 不会回收 strongRef 指向的对象strongRef=null;// 断开引用,对象变得可回收四、软引用(Soft Reference)
1. 定义
软引用用于描述有用但不是必须的内存缓存。在系统将要发生内存溢出之前,GC 会将这些软引用对象列入回收范围进行第二次回收。如果这次回收后内存仍不足,才抛出 OOM。
2. 特点
- 适合实现内存敏感的高速缓存(如图片缓存、查询结果缓存)。
- 回收时机:JVM 根据当前内存状况和
-XX:SoftRefLRUPolicyMSPerMB参数决定。默认值 1000 意味着每 MB 堆空间中的软引用存活 1 秒(基于上次访问时间)。 - 软引用对象在回收前可以被 JVM 保留一段时间,不是立刻清除。
3. 代码示例
importjava.lang.ref.SoftReference;publicclassSoftRefDemo{publicstaticvoidmain(String[]args){Objectobj=newObject();SoftReference<Object>softRef=newSoftReference<>(obj);obj=null;// 取消强引用// 第一次获取,对象还存在System.out.println(softRef.get());// 非 null// 模拟内存压力(可以通过 -Xms10m -Xmx10m 运行并分配大量内存)// 在内存不足时,softRef.get() 可能会返回 null}}4. 典型应用
- 缓存实现:例如 Android 的图片缓存、MyBatis 的二级缓存(可选软引用)。
- 内存敏感的加载器:在内存压力下释放部分资源。
五、弱引用(Weak Reference)
1. 定义
弱引用的强度比软引用更低。下一次 GC 发生时,无论内存是否充足,弱引用指向的对象都会被回收(只要没有强引用或软引用)。
2. 特点
- 更短的生命周期。
- 常用于实现规范映射(canonical mapping),如
WeakHashMap中的键使用弱引用,当键对象没有其他强引用时,自动从 Map 中移除,避免内存泄漏。
3. 代码示例
importjava.lang.ref.WeakReference;publicclassWeakRefDemo{publicstaticvoidmain(String[]args){Objectobj=newObject();WeakReference<Object>weakRef=newWeakReference<>(obj);obj=null;// 取消强引用System.out.println(weakRef.get());// 非 nullSystem.gc();// 手动触发 GCSystem.out.println(weakRef.get());// 很可能输出 null}}4. 典型应用
- WeakHashMap:键是弱引用,当键只有被 Map 引用时,下次 GC 会自动删除该键值对,常用于缓存元数据或避免监听器泄漏。
- ThreadLocal:
ThreadLocalMap中的 Entry 继承了WeakReference,键(ThreadLocal 实例)是弱引用,防止线程池中的线程存活导致 ThreadLocal 无法回收。
// WeakHashMap 演示WeakHashMap<Object,String>map=newWeakHashMap<>();Objectkey=newObject();map.put(key,"value");System.out.println(map.size());// 1key=null;System.gc();// 稍后 map 中的条目可能已被自动清除六、虚引用(Phantom Reference)
1. 定义
虚引用是最弱的引用,它无法通过 get() 方法获取实际对象(始终返回null)。它的唯一作用是在对象被 GC 回收时收到一个通知。虚引用必须与引用队列(ReferenceQueue)配合使用。
2. 特点
- 无法通过虚引用访问对象。
- 只有虚引用指向的对象被回收后,虚引用才会被放入引用队列。
- 常用于管理堆外内存(如 DirectByteBuffer 的 Cleaner 机制),在对象回收时执行清理动作。
3. 代码示例
importjava.lang.ref.PhantomReference;importjava.lang.ref.ReferenceQueue;publicclassPhantomRefDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{Objectobj=newObject();ReferenceQueue<Object>refQueue=newReferenceQueue<>();PhantomReference<Object>phantomRef=newPhantomReference<>(obj,refQueue);obj=null;// 取消强引用System.out.println(phantomRef.get());// 总是 nullSystem.gc();// 稍后,从队列中取出虚引用(表示对象已回收)System.out.println(refQueue.remove(1000));// 可能为 phantomRef}}4. 典型应用
- 堆外内存回收:Netty、DirectByteBuffer 使用
sun.misc.Cleaner(虚引用子类)来释放堆外内存。 - 资源清理:当对象被回收时,需要关闭文件、释放网络连接等,但一般使用
try-finally或try-with-resources更可靠。
注意:虚引用的清理动作通常由 JVM 内部线程执行,开发者很少直接使用。JDK 9 引入了
Cleaner类,比finalize()更安全。
七、引用队列(ReferenceQueue)
1. 定义
引用队列是引用的“通知机制”。当软引用、弱引用、虚引用所引用的对象被 GC 回收后,这些引用对象(SoftReference等实例)本身会被加入关联的引用队列。开发者可以轮询队列,进行一些后置清理工作。
2. 使用方式
ReferenceQueue<Object>queue=newReferenceQueue<>();WeakReference<Object>weakRef=newWeakReference<>(obj,queue);// 当 obj 被回收后,weakRef 会被自动放入 queue3. 作用
- 避免内存泄漏:及时清除不再使用的引用对象(这些引用对象本身也占用内存)。
- 执行额外清理:例如虚引用清理堆外内存。
八、对比总结
| 引用类型 | 回收时机 | 能否获取对象 | 典型应用 | 是否需要引用队列 |
|---|---|---|---|---|
| 强引用 | 永不回收(除非断开) | 能 | 普通对象引用 | 否 |
| 软引用 | 内存不足时 | 能 | 内存敏感缓存 | 可选(用于清除过期缓存) |
| 弱引用 | 下次 GC 必回收 | 能 | WeakHashMap、ThreadLocal | 可选 |
| 虚引用 | 任何时候(无法获取) | 不能 | 堆外内存清理 | 必须 |
九、常见面试追问
Q1:软引用和弱引用的区别?
- 软引用只有内存不足时才回收,弱引用只要 GC 就回收。因此软引用更适合实现缓存(希望尽量保留),弱引用更适合规范映射(不阻碍对象回收)。
Q2:WeakHashMap的工作原理是什么?
WeakHashMap的 Entry 继承了WeakReference,键是弱引用。当键对象不再被外部强引用时,GC 会回收该键,同时将 Entry 放入引用队列。WeakHashMap在读取时会自动清理这些无效 Entry,所以不需要手动删除。
Q3:为什么要有虚引用?用finalize()不行吗?
finalize()方法执行不确定且会导致对象“复活”,性能差且已被 JDK 9 废弃。虚引用提供了一种确定、低开销的回收通知机制,并且不干扰对象的生命周期。Cleaner是虚引用的升级版。
Q4:软引用何时被回收?可以控制回收优先级吗?
通过 JVM 参数-XX:SoftRefLRUPolicyMSPerMB=<ms>可以调整软引用的“存活时间”。默认 1000 毫秒,表示每 1 MB 堆空间允许软引用在最后一次访问后存活 1 秒。该值越大,软引用越不容易被回收。
Q5:如何选择使用哪种引用?
- 普通对象 → 强引用
- 实现缓存,希望尽量保留但内存紧张时释放 → 软引用
- 实现 Map 的键,不阻碍键对象回收 → 弱引用
- 需要在对象回收时执行清理,且无法通过 finalize 实现 → 虚引用 + 引用队列
十、总结
| 引用类型 | 一句话总结 |
|---|---|
| 强引用 | 只要我不放手,你就别想走 |
| 软引用 | 内存还有你就留着,不够了就卖掉 |
| 弱引用 | 下次打扫卫生,你就会消失 |
| 虚引用 | 我看不见你,但知道你已经走了 |
理解这四种引用类型,不仅能帮你写出更健壮的缓存和避免内存泄漏,还能深入理解 JVM 和 Android 的内存管理机制。希望这篇文章能帮你彻底掌握这个高频考点,欢迎继续讨论。