Java 的引用
在 JVM 中,引用(reference)是一种数据类型,用于定位某个对象(或者 null)。垃圾收集器正是沿着这种引用关系来推导对象是否存活、是否应当被回收。在传统认知里,对象的状态是非黑即白的——要么从 GC Roots 出发沿(强)引用链可达(存活),要么不可达(被判定为垃圾并回收)。
大部分场景下这个设计没有问题,但存在一些特殊需求:
- 弹性缓存设计:某些计算成本高昂但不致命的缓存数据,希望在内存充足时保留以提升性能,在内存吃紧时自动回收,从而避免 OOM。
- 生命周期解耦:在事件监听器或回调机制中,如果注册中心持有目标对象的强引用,会导致目标对象在业务生命周期结束后依然无法被回收,从而引发内存泄漏。
- 特殊资源的清理:某些 GC 无法直接管理的资源(如堆外内存),需要在对应的 Java 对象被销毁时触发安全的清理逻辑。
为了赋予开发者更灵活的内存控制能力,Java 从 JDK 1.2 开始引入了软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)。它们允许开发者在一定程度上介入对象的生命周期管理。
虽然本文主要就是在介绍这三种引用,除非你完全清楚其背后的运行逻辑,并且拥有十分明确的不可避免的使用理由,否则不要使用这些引用类及其浅层封装!!!
强引用
强引用就是最传统的引用关系,即代码中随处可见的Object o = new Object()这种引用赋值。从 GC Roots 出发沿强引用链可达就会视为存活对象,不会被垃圾收集器回收。
软引用(SoftReference)
仅软引用可达的对象在内存充足时不会被垃圾收集器回收;在发生内存压力时,GC 会根据策略回收它们。JVM 保证在抛出堆空间不足的 OOM 之前,会清空所有指向“仅软引用可达”对象的软引用;换而言之只有在清除软引用仍无法腾出足够内存时,才会抛出 OOM 异常
软引用试图把缓存驱逐策略内嵌进引用模型,从设计动机上看有一定合理性,但在实践中基本是失败的,JVM 并没有足够的上下文来判断哪个对象“更值得保留”。
应用
没有应用,不要使用软引用!!!
问题
历史上软引用曾被用作缓存(哪怕是现在 SoftReference 的 java doc 中也推荐其应用场景为缓存),但其存在严重的缓存雪崩问题。软引用被回收的时机不可预测,高并发下大量 key 同时失效,重建开销集中爆发,而程序员对此几乎毫无控制能力。有缓存需求请使用 Caffeine 等缓存组件
此外,软引用需要额外处理,会对 GC 带来额外处理开销,有可能造成更长的停顿时间
弱引用(WeakReference)
仅弱引用可达的对象会被垃圾收集器直接回收,与软引用不同,弱引用不试图干涉 GC 的回收决策。
弱引用的本质是解耦对象的所有权与生命周期,是一个合理的语义补充
应用
监听器/回调防内存泄漏
这是最典型的用法。如果用强引用持有监听器,注册方忘记反注册就会造成泄漏。用弱引用持有监听器,宿主对象生命周期结束后自然被回收,框架侧不需要依赖调用方的纪律性
附加元数据
WeakHashMap的核心场景:key 是某个外部对象,想附加一些元数据,但不想因为这个 Map 的存在就延长 key 对象的生命周期。
规范化映射
用于确保内存中逻辑相等的对象只有一份物理实例。如果用强引用做享元池,池子会不断膨胀导致内存泄漏;利用弱引用持有池中的规范实例,可以在外部业务不再使用该实例时,允许 GC 将其自动回收
问题
弱引用的逻辑本身没有问题,但所有弱引用操作本质上都是业务线程与 GC 线程之间的竞态,使用时需要格外注意
尤其注意WeakHashMap的使用,它存在很多隐含且难以调试的竞态问题
虚引用(PhantomReference)
一个对象是否有虚引用存在,完全不会对其生命周期构成影响,甚至无法通过虚引用取得对象实例。为对象设置虚引用关联的唯一目的,是在该对象被回收时收到一个通知
虚引用本质上是 Java 缺乏可靠析构函数的补偿机制。这个工作本来应该由finalize()完成,但finalize存在一系列严重缺陷:对象复活、GC 需要单独处理一轮导致的性能问题、只执行一次、执行线程不可控、执行时机不确定、Finalizer 队列积压等。虚引用通过一个关键设计规避了其中最棘手的问题:它不暴露原对象的引用,业务清理逻辑无法访问原对象,对象复活的路被彻底断掉了。此外它的执行线程也是可控的,由开发者手动处理ReferenceQueue来驱动
java.lang.ref.Cleaner就是虚引用的浅层封装,会自己管理一个守护线程
应用
在业务代码中没有应用,不要在业务代码中使用虚引用!!!
问题
持有外部资源的对象在销毁时释放资源,这个需求确实存在。但由于可达性分析的固有局限,析构的执行时机永远无法保证实时性。资源应当使用try-with-resources手动关闭,而不是依赖 GC 清理,甚至不应该用虚引用来兜底,它本质上是在掩盖问题,而不是解决问题
尽管 JDK 和很多开源框架/库中大量使用 Cleaner 进行兜底,但是这是建立在面对的是不可信的调用方的基础上的。如果你写的不是 JDK 或框架/库,不要使用虚引用(DirectByteBuffer 除外,它设计有问题导致只能用 Cleaner 关闭)