【JVM原理详解】23-四种引用类型-强软弱虚

📅 2026/7/29 19:12:54 👁️ 阅读次数 📝 编程学习
【JVM原理详解】23-四种引用类型-强软弱虚

23-四种引用类型:强、软、弱、虚

前两篇讨论的 GC Roots 和可达性分析,本质是在回答"对象是否被引用"。但现实中我们常常希望对"引用"做更精细的控制:比如缓存里的对象,内存够用就保留、不够再回收;又比如有些资源需要在对象被回收前执行一段清理逻辑。Java 提供了四种引用类型来满足这些需求——StrongReference、SoftReference、WeakReference、PhantomReference。本篇将逐一剖析它们的语义、实现与典型应用。

引用强度的设计意图

JDK 1.2 之前,Java 只有"引用"这一种概念——只要引用存在,对象就永不回收。这对缓存、临时映射等场景过于刚性。于是在 1.2 引入了java.lang.ref包,将引用划分为四个级别,从强到弱依次为:

强引用 (Strong) > 软引用 (Soft) > 弱引用 (Weak) > 虚引用 (Phantom)

引用越"弱",对象被回收的优先级越高。这套机制让开发者可以在"对象可达性"之外再叠加一层"回收优先级"的语义。

StrongReference:永不主动回收

StrongReference 就是我们日常写的普通引用:

Objectobj=newObject();// 强引用

只要强引用还存在,垃圾收集器永远不会回收被引用的对象——哪怕 OOM 也不会。这是 JVM 规范的硬性保证,也是其他三种引用的对比基准。

工程上绝大多数内存泄漏都源于"本应是临时引用的强引用被意外长期持有",比如:

publicclassLeakyCache{// 强引用持有,永不回收privatestaticfinalMap<String,byte[]>CACHE=newHashMap<>();publicstaticvoidput(Stringkey,byte[]data){CACHE.put(key,data);}// 没有 remove 方法 → 内存泄漏}

避免强引用泄漏的核心是"显式置 null"或"用更弱的引用替代"。

SoftReference:内存不足时回收

SoftReference 描述"还有用但非必需"的对象。在 JVM 抛出 OOM 之前,会清理软引用对象;清理后仍内存不足才会真正 OOM。

典型应用:缓存

importjava.lang.ref.SoftReference;importjava.util.HashMap;importjava.util.Map;/** * 基于软引用的简单缓存 * 适用 JDK 8/11/17 */publicclassSoftCache<K,V>{privatefinalMap<K,SoftReference<V>>cache=newHashMap<>();publicvoidput(Kkey,Vvalue){cache.put(key,newSoftReference<>(value));}publicVget(Kkey){SoftReference<V>ref=cache.get(key);returnref==null?null:ref.get();}}

SoftReference.get()在对象被回收后返回null,调用方需要处理这种"缓存未命中"的情况。

回收时机

HotSpot 对软引用的回收遵循最近最少使用原则:最近被访问过的软引用会保留更久。这是通过-XX:SoftRefLRUPolicyMSPerMB(默认 1000ms)参数控制的:堆中每剩余 1MB 空间,软引用的存活时间增加约 1 秒。

# 调大软引用存活时长(每 MB 多活 2 秒)java-XX:SoftRefLRUPolicyMSPerMB=2000-cpMyApp com.example.Main

需要注意:G1 之后默认开启-XX:+UseG1GC,软引用回收策略略有变化,G1 通过-XX:G1SoftRefLRUPolicyMSPerMB单独控制。

SoftReference 的代价

软引用对象本身(SoftReference实例)是强引用,它内部通过一个referent字段"弱指向"真正的对象。GC 在回收 referent 前会先检查软引用条件,满足则把 referent 字段置 null、并把 SoftReference 实例放入关联的 ReferenceQueue。这意味着软引用本身仍占内存,需要配合队列清理。

WeakReference:下次 GC 即回收

WeakReference 比 SoftReference 更弱——只要发生 GC,无论内存是否充足,弱引用指向的对象都会被回收(前提是没有其他强引用)。

典型应用:WeakHashMap

importjava.util.WeakHashMap;classUserHolder{privatestaticfinalWeakHashMap<Object,String>META=newWeakHashMap<>();publicstaticvoidbind(Objectkey,Stringtag){META.put(key,tag);}// 当 key 对象在外部失去引用后,下次 GC 会自动清理该 entry}

WeakHashMap的 key 是弱引用,value 是强引用。这种"key 弱、value 强"的结构有一个潜在陷阱:value 本身可能反向引用 key,导致 key 实际上无法回收。这类问题排查难度较大,建议 value 持有的对 key 的引用尽量用弱引用。

ThreadLocal 的实现

ThreadLocal内部也是用WeakReference持有 ThreadLocal 实例本身:

// ThreadLocal.ThreadLocalMap.Entry 源码片段staticclassEntryextendsWeakReference<ThreadLocal<?>>{Objectvalue;Entry(ThreadLocal<?>k,Objectv){super(k);// key 是弱引用value=v;// value 是强引用}}

这就解释了为什么 ThreadLocal 实例本身可以被回收,但value 仍可能泄漏:value 强引用了真实数据,只要线程不死,value 就不会被回收。所以使用 ThreadLocal 必须remove()

PhantomReference:对象回收前通知

PhantomReference 是最弱的一种:通过它甚至无法获取到对象本身——get()永远返回null。它的唯一作用是在对象被回收时收到"通知",常用于资源清理

ReferenceQueue 工作原理

四种引用都可以在创建时关联一个ReferenceQueue。GC 回收 referent 后,会把对应的 Reference 对象入队(enqueue)。应用线程从队列中取出 Reference,就能知道哪个对象被回收了。

创建引用 GC 回收 referent Reference + Queue ──────────────────────> Reference 入队 | 应用线程 poll → 执行清理

虚引用必须配合 ReferenceQueue 使用,否则毫无意义——因为它的get()永远是 null,没有队列就没法感知回收事件。

Cleaner 机制

JDK 9 引入了java.lang.ref.Cleaner,它本质上是一个虚引用 + ReferenceQueue 的封装,替代了已过时的finalize()

importjava.lang.ref.Cleaner;/** * 使用 Cleaner 管理本地资源 * 适用 JDK 9+ */publicclassNativeResourceimplementsAutoCloseable{privatestaticfinalCleanerCLEANER=Cleaner.create();privatestaticclassStateimplementsRunnable{privatelongnativeHandle;// 模拟本地句柄State(longhandle){this.nativeHandle=handle;}@Overridepublicvoidrun(){// 对象被回收时回调if(nativeHandle!=0){freeNative(nativeHandle);nativeHandle=0;}}}privatefinalStatestate;privatefinalCleaner.Cleanablecleanable;publicNativeResource(longhandle){this.state=newState(handle);this.cleanable=CLEANER.register(this,state);}@Overridepublicvoidclose(){cleanable.clean();// 主动清理}privatestaticnativevoidfreeNative(longhandle);}

关键点:

  • State不持有外部对象NativeResource的引用,避免阻止其回收。
  • Cleaner.register(this, state)内部把this用虚引用包装、关联到 Cleaner 自己的队列。
  • NativeResource被回收时,Cleaner 线程会执行state.run()完成清理。
  • 也可以主动调用cleanable.clean()提前清理(幂等)。

PhantomReference vs finalize

finalize()在 JDK 9 被标记 deprecated,JDK 18 起默认禁用。它的核心问题是:可以"复活"对象、执行时机不确定、执行线程无保证。Cleaner 通过独立的清理线程不可访问 referent的设计规避了这些问题。

完整对比示例

下面用一段代码同时演示四种引用在 GC 前后的行为差异:

importjava.lang.ref.*;importjava.util.ArrayList;importjava.util.List;publicclassReferenceDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ReferenceQueue<Object>queue=newReferenceQueue<>();// 1. 强引用Objectstrong=newObject();// 2. 软引用SoftReference<Object>soft=newSoftReference<>(newObject(),queue);// 3. 弱引用WeakReference<Object>weak=newWeakReference<>(newObject(),queue);// 4. 虚引用PhantomReference<Object>phantom=newPhantomReference<>(newObject(),queue);// 检查引用是否还在System.out.println("Before GC:");System.out.println(" strong = "+(strong!=null));System.out.println(" soft = "+(soft.get()!=null));System.out.println(" weak = "+(weak.get()!=null));System.out.println(" phantom = "+(phantom.get()!=null));// 总是 null// 触发 GCSystem.gc();Thread.sleep(500);System.out.println("After GC:");System.out.println(" strong = "+(strong!=null));System.out.println(" soft = "+(soft.get()!=null));// 内存够时仍存活System.out.println(" weak = "+(weak.get()!=null));// 通常被回收System.out.println(" phantom = "+(phantom.get()!=null));// null// 从队列中取出已回收的引用Reference<?>ref;while((ref=queue.poll())!=null){System.out.println(" 回收通知: "+ref.getClass().getSimpleName());}}}

典型输出(内存充足时):

Before GC: strong = true soft = true weak = true phantom = false After GC: strong = true soft = true weak = false phantom = false 回收通知: WeakReference 回收通知: PhantomReference

软引用在内存充足时不会被回收,只有在 OOM 前才被清理;弱引用一遇 GC 即清;虚引用的 referent 被回收后会触发入队。

实践要点

1. 缓存优先用 Caffeine / Guava

虽然 SoftReference 可以做缓存,但回收时机依赖 GC 策略,不便于容量规划。生产场景更推荐 Caffeine,它支持weakKeys()/weakValues()/softValues()配置,并自带 LRU/LFU 策略,可控性远高于手工实现。

2. WeakHashMap 不是并发安全的

WeakHashMap没有同步机制,并发场景需用Collections.synchronizedMap包裹或改用ConcurrentHashMap+WeakReference自行实现。

3. ReferenceQueue 必须主动消费

如果关联了队列但从不poll,队列中的 Reference 对象会一直占用内存——这是隐性泄漏。Cleaner 内部已经处理了这个问题,所以优先用 Cleaner。

4. 虚引用的 referent 在入队前已被回收

注意:JDK 实现中,PhantomReference 是在 referent 被** finalize 后、内存释放前**入队的。这意味着此时 referent 已无法访问,但仍"占用内存"直到 Cleaner 执行完。所以虚引用不能用于"复活"对象。

5. 软引用在 G1/ZGC 下的行为

G1 对软引用采用"分区回收"策略,可能延迟回收部分软引用;ZGC 则在并发标记阶段处理。生产中如果发现软引用占用偏高,可调整SoftRefLRUPolicyMSPerMB或改用显式 TTL 缓存。

小结

  • 强引用永不回收,是日常引用的默认形态,也是泄漏主因。
  • 软引用在 OOM 前回收,适合内存敏感的缓存;存活时长由SoftRefLRUPolicyMSPerMB控制。
  • 弱引用下次 GC 即回收,WeakHashMapThreadLocal都基于它。
  • 虚引用无法获取对象,仅在对象回收前通过 ReferenceQueue 通知,配合Cleaner替代finalize()
  • ReferenceQueue 是引用与回收通知的桥梁,必须主动消费。

下一篇我们将从"判定对象存活"过渡到"如何回收",深入对比四种经典垃圾回收算法。

更多内容:JVM调优实战