强、软、弱、虚引用
概述
在 Java 中,我们通常通过 Object obj = new Object()
这样的方式创建对象,这里的 obj
就是一个强引用。但除了强引用之外,Java 还提供了三种其他类型的引用:软引用、弱引用 和 虚引用。它们的主要区别在于垃圾回收器在不同时机回收它们所指向的对象,从而为开发者提供了更灵活的内存控制手段。
这四种引用的强度由高到低依次是:强引用 > 软引用 > 弱引用 > 虚引用。
1. 强引用
定义
强引用是我们最常见、最普遍的引用类型。只要一个对象存在强引用与之关联,垃圾回收器就绝对不会回收它,即使在内存不足时,JVM 宁愿抛出 OutOfMemoryError
异常,也不会回收这些对象。
代码示例
public class StrongReferenceExample {public static void main(String[] args) {// 创建一个对象,并建立一个强引用Object obj = new Object(); // 强引用// 即使系统内存紧张,只要 obj 变量还存活,new Object() 创建的对象就不会被回收System.gc(); // 主动建议 GC 回收,但 GC 不会回收这个对象// 当强引用被置为 null 时,对象就不再被强引用obj = null;// 此时,这个对象就变成了可回收的对象,在下次 GC 时可能会被回收System.gc(); // 现在 GC 有可能回收这个对象了}
}
特点
默认引用类型:我们日常编写的代码中,99% 都是强引用。
永不回收:只要引用存在,GC 就不回收。
可能导致内存泄漏:如果强引用使用不当(例如,静态集合中存储了大量不再使用的对象),会导致这些对象无法被回收,从而引发内存泄漏。
应用场景
所有普通对象:程序中绝大多数对象的默认生命周期管理方式。
关键性数据:对于程序运行必须存在的、不能被回收的核心数据,必须使用强引用。
2. 软引用
定义
软引用用来描述一些还有用但并非必需的对象。在内存足够时,垃圾回收器不会回收软引用关联的对象;但是,当 JVM 认为内存不足时(即将发生 OOM 之前),就会回收这些对象。软引用通常用来实现内存敏感的缓存。
代码示例
import java.lang.ref.SoftReference;public class SoftReferenceExample {public static void main(String[] args) {// 创建一个对象Object obj = new Object();// 创建一个软引用,指向这个对象SoftReference<Object> softRef = new SoftReference<>(obj);// 断开强引用obj = null;// 通过软引用获取对象System.out.println("Before GC: " + softRef.get()); // 可以获取到对象// 主动触发 GC,但此时内存可能充足,GC 不一定回收System.gc();System.out.println("After GC (memory is enough): " + softRef.get()); // 很可能还能获取到对象// 模拟内存紧张的情况(这个模拟比较复杂,实际效果取决于JVM实现和内存压力)// 在真实场景中,当系统内存耗尽前,GC会回收软引用对象// 我们可以通过创建大量对象来消耗内存,从而观察软引用被回收try {// 尝试消耗内存,迫使 JVM 回收软引用byte[] bytes = new byte[100 * 1024 * 1024]; // 分配 100MBSystem.out.println("Allocated 100MB, now check soft ref: " + softRef.get()); // 此时很可能为 null} catch (OutOfMemoryError e) {System.out.println("OOM occurred, soft ref was: " + softRef.get()); // 此时一定为 null}}
}
特点
内存敏感:回收策略与 JVM 当前内存状况紧密相关。
“缓存”特性:非常适合做缓存,在内存充足时缓存数据,在内存不足时自动释放缓存以避免 OOM。
get()
方法:可以通过 softRef.get()
获取到被引用的对象。如果对象已被回收,则返回 null
。
应用场景
内存敏感的高速缓存:
图片缓存:在 Android 或 Web 应用中,从网络加载的图片可以用软引用缓存起来。当内存足够时,用户再次查看同一图片时可以直接从内存加载,速度快;当内存不足时,系统会自动回收这些图片缓存,保证应用不崩溃。
网页缓存:浏览器可以将已访问的页面内容用软引用缓存。
复杂数据的缓存:例如,数据库查询结果、解析后的 XML/JSON 数据等,这些数据重新生成的成本较高,但又不是程序运行所必需的。
3. 弱引用
定义
弱引用用来描述非必需的对象。它的强度比软引用更弱。无论当前内存是否足够,只要垃圾回收器发现了只具有弱引用的对象,就一定会回收它。回收的速度和时机取决于 GC 的调度。
代码示例
import java.lang.ref.WeakReference;public class WeakReferenceExample {public static void main(String[] args) {Object obj = new Object();// 创建一个弱引用WeakReference<Object> weakRef = new WeakReference<>(obj);// 断开强引用obj = null;System.out.println("Before GC: " + weakRef.get()); // 可以获取到对象// 主动触发 GCSystem.gc();// GC 执行后,弱引用关联的对象会被回收// 注意:System.gc() 只是建议,不是立即执行,所以这里可能需要稍作等待或多次调用// 但在大多数情况下,下一次 GC 时它会被回收try {Thread.sleep(100); // 稍微等待一下,确保 GC 完成} catch (InterruptedException e) {e.printStackTrace();}System.out.println("After GC: " + weakRef.get()); // 几乎总是 null}
}
特点
发现即回收:只要 GC 线程扫描到了它,就会被回收,与内存是否充足无关。
生命周期更短:通常在下一次 GC 发生时就会被回收。
get()
方法:同样可以通过 weakRef.get()
获取对象,如果对象已被回收,则返回 null
。
应用场景
WeakHashMap:这是弱引用最经典的应用。WeakHashMap 的键是弱引用。当某个键不再被外部强引用时,即使它还在 WeakHashMap 中,GC 也会回收它所指向的 Entry。这非常适合用来存储元数据或临时关联信息。
场景:例如,在一个 Web 服务器中,可以用 WeakHashMap 来存储用户会话的额外信息,键是 Session 对象。当用户会话过期(Session 对象的强引用被移除)后,WeakHashMap 中对应的条目会自动被移除,无需手动清理,避免了内存泄漏。
监听器和回调:在实现观察者模式或事件监听时,如果监听器被注册后,但它的生命周期比被观察者短,就容易造成内存泄漏(被观察者持有监听器的强引用)。如果使用弱引用来持有监听器,当监听器不再被使用时,GC 可以回收它,从而自动解除注册。
4. 虚引用
定义
虚引用是所有引用类型中最弱的一个。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例。它的唯一目的就是在一个对象被 GC 回收时,能够收到一个系统通知。
代码示例
虚引用必须和 ReferenceQueue
(引用队列)联合使用。当 GC 准备回收一个对象时,如果发现它有虚引用,就会在回收对象之前,将这个虚引用加入到与之关联的 ReferenceQueue
中。
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;public class PhantomReferenceExample {public static void main(String[] args) throws InterruptedException {Object obj = new Object();ReferenceQueue<Object> refQueue = new ReferenceQueue<>();// 创建一个虚引用,必须关联一个引用队列PhantomReference<Object> phantomRef = new PhantomReference<>(obj, refQueue);// 断开强引用obj = null;System.out.println("PhantomRef.get(): " + phantomRef.get()); // 永远是 null// 主动触发 GCSystem.gc();// GC 后,虚引用会被放入引用队列// 需要一个独立的线程来监控队列Thread monitorThread = new Thread(() -> {try {Reference<?> refFromQueue = refQueue.remove(); // 阻塞,直到有引用进入队列System.out.println("Object was collected. Phantom reference is in queue: " + refFromQueue);// 在这里可以执行一些清理工作,比如释放 native memory// 清理完成后,最好将虚引用本身清空,避免它再次入队refFromQueue.clear();} catch (InterruptedException e) {e.printStackTrace();}});monitorThread.setDaemon(true);monitorThread.start();// 主线程等待一下,确保监控线程有时间工作Thread.sleep(1000);System.out.println("Main thread finished.");}
}
特点
get()
永远返回 null
:无法通过虚引用获取到对象。
必须配合 ReferenceQueue
:它的唯一价值在于当对象被回收时,GC 会将虚引用放入队列,起到一个“通知”作用。
不影响对象生命周期:虚引用的存在与否,和对象是否被回收完全无关。
应用场景
管理堆外内存:这是虚引用最主要和最核心的应用。Java 的 NIO 库中的 DirectByteBuffer 就是利用虚引用来管理直接内存(堆外内存)的。
原理:DirectByteBuffer 在分配堆外内存时,会同时创建一个指向它的 Cleaner(Cleaner 是 PhantomReference 的子类)。当 DirectByteBuffer 对象本身被 GC 回收时,它的 Cleaner 对象会被放入 ReferenceQueue。一个后台线程会监控这个队列,当发现 Cleaner 后,会调用它的 clean() 方法,该方法会执行底层的 free() 调用,从而释放对应的堆外内存。这实现了堆外内存的自动、安全管理。
对象销毁前的资源清理:当需要进行比 Java 的 finalize() 方法更可靠、更可控的资源清理时,可以使用虚引用。finalize() 方法是不可靠的(执行时机不确定,可能不被执行),而虚引用机制提供了一个明确的、由 GC 触发的清理点。
总结与对比
引用类型 | 回收时机 | get() 方法 | 主要用途 | 关键点 |
---|---|---|---|---|
强引用 | 永不回收(除非显式置为 null ) | 返回对象本身 | 普通对象、核心数据 | 默认引用,可能导致内存泄漏 |
软引用 | 内存不足时(OOM 之前) | 返回对象,可能为 null | 内存敏感的缓存 | “缓存”,平衡性能和内存 |
弱引用 | 下次 GC 扫描到时(与内存无关) | 返回对象,可能为 null | WeakHashMap 、临时监听器 | “临时关联”,自动清理 |
虚引用 | 对象被回收时(无法影响回收) | 永远返回 null | 堆外内存管理、资源清理 | “通知”,必须配合 ReferenceQueue |
使用建议
- 默认使用强引用:对于绝大多数业务对象,强引用是正确且高效的选择。
- 需要缓存时,考虑软引用:当你想实现一个缓存,并希望在内存紧张时能自动释放缓存以避免 OOM,软引用是最佳选择。
- 需要自动清理关联时,考虑弱引用:当你需要将一个对象与另一个对象关联,但又不希望这种关联影响被关联对象的回收(如
WeakHashMap
),或者需要管理生命周期较短的监听器时,弱引用非常合适。 - 需要精确的回收后操作时,考虑虚引用:当你需要在对象被 GC 回收后执行一些必须的清理操作,特别是涉及到 JVM 之外的资源(如堆外内存、文件句柄等)时,虚引用是专业且可靠的选择。对于普通开发者来说,这个场景相对较少。