Java 的引用

在 Java 的内存模型中,如果某段内存存储的数据能够定位到另一块内存的起始地址,我们便称之为引用。垃圾收集器正是依赖这种引用关系来判断对象是否存活、是否应当被回收。在传统认知里,对象的引用状态是非黑即白的——要么被引用(存活),要么毫无引用(被判定为垃圾并回收)。

大部分场景下这个设计没有问题,但存在一些特殊需求:

  1. 弹性缓存设计:某些计算成本高昂但不致命的缓存数据,希望在内存充足时保留以提升性能,在内存吃紧时自动回收,从而避免 OOM。
  2. 生命周期解耦:在事件监听器或回调机制中,如果注册中心持有目标对象的强引用,会导致目标对象在业务生命周期结束后依然无法被回收,从而引发内存泄漏。
  3. 特殊资源的清理:某些 GC 无法直接管理的资源(如堆外内存),需要在对应的 Java 对象被销毁时触发安全的清理逻辑。

为了赋予开发者更灵活的内存控制能力,Java 从 JDK 1.2 开始扩充了引用的分类,引入了软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)。它们允许开发者在一定程度上介入对象的生命周期与 GC 过程。

虽然本文主要就是在介绍这三种引用,但是除非你完全清楚其背后的运行逻辑,并且拥有十分明确的不可避免的使用理由,否则不要使用这些引用类及其浅层封装!!!

本文缺乏代码示例,但是由于作者不建议读者真的用这些功能所以不准备补充

强引用

强引用就是最传统的引用关系,即代码中随处可见的 Object o = new Object() 这种引用赋值。无论何种情况下,只要存活对象与被引用对象之间存在强引用关系,垃圾收集器就不会回收被引用对象。

软引用(SoftReference)

只有软引用指向的对象在大多数情况下不会被垃圾收集器回收,在发生内存压力或对象长时间未被访问时,GC 会根据策略回收它们,JVM 保证在抛出 OOM 之前,会清空所有软引用。如果清空软引用后仍没有足够内存,才会抛出 OOM 异常。

软引用试图把缓存驱逐策略内嵌进引用模型,从设计动机上看有一定合理性,但在实践中几乎是失败的,JVM 并没有足够的上下文来判断哪个对象"更值得保留"。

应用

没有应用,不要使用软引用!!!

问题

历史上软引用曾被用作缓存,但存在严重的缓存击穿问题。软引用被回收的时机不可预测,高并发下大量 key 同时失效,重建开销集中爆发,而程序员对此毫无控制能力。有缓存需求请使用 Caffeine 等缓存组件

此外,软引用需要额外处理,会对 GC 带来额外处理开销,有可能造成更长的停顿时间

弱引用(WeakReference)

只有弱引用指向的对象会被垃圾收集器直接回收,与软引用不同,弱引用不试图干涉 GC 的回收决策。

弱引用的本质是解耦对象的所有权与生命周期,是一个合理的语义补充

应用

监听器/回调防内存泄漏

这是最典型的用法。如果用强引用持有监听器,注册方忘记反注册就会造成泄漏。用弱引用持有监听器,宿主对象生命周期结束后自然被回收,框架侧不需要依赖调用方的纪律性

规范化映射

WeakHashMap 的核心场景:key 是某个外部对象,想附加一些元数据,但不想因为这个 Map 的存在就延长 key 对象的生命周期。比如给任意对象附加属性、实现对象池的弱键索引等

问题

弱引用的逻辑本身没有问题,但所有弱引用操作本质上都是业务线程与 GC 线程之间的竞态,使用时需要格外注意,不过好在 GC 线程只会清理不会写入

尤其注意 WeakHashMap 的使用,它存在海量隐含且难以调试的竞态问题

此外不要使用弱引用做缓存,原因同软引用

虚引用(PhantomReference)

虚引用不是一个真正的引用关系。一个对象是否有虚引用存在,完全不会对其生命周期构成影响,甚至无法通过虚引用取得对象实例。为对象设置虚引用关联的唯一目的,是在该对象被回收时收到一个通知

虚引用本质上是 Java 缺乏可靠析构函数的补偿机制。这个工作本来应该由 finalize() 完成,但 finalize 存在一系列严重缺陷:对象复活、GC 需要单独处理一轮导致的性能问题、只执行一次、执行线程不确定、执行时机不确定、Finalizer 队列积压等。虚引用通过一个关键设计规避了其中最棘手的问题:它不持有原对象的引用,清理逻辑在物理上就无法访问原对象,对象复活的路被彻底断掉了。执行线程和时机也因此变得可控,由开发者手动处理 ReferenceQueue 来驱动

Cleaner 就是虚引用的浅层封装

应用

没有应用,不要使用虚引用!!!

问题

持有外部资源的对象在销毁时释放资源,这个需求确实存在。但由于可达性分析的固有局限,析构的执行时机永远无法保证实时性。资源应当使用 try-with-resources 手动关闭,而不是依赖 GC 清理。甚至不应该用虚引用来兜底,它本质上是在掩盖问题,而不是解决问题:在测试环境堆内存充裕时 Cleaner 可能长期不触发,问题完全不可见,等到生产环境内存压力集中时才爆发,届时已很难定位根因

尽管 JDK 和很多开源框架/库中大量使用 Cleaner 进行兜底,但是这是建立在面对的是不可信的调用方的基础上的。如果你写的不是 JDK 或框架/库,不要使用虚引用

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐