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

发布时间:2026/7/29 18:56:01
【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 就是我们日常写的普通引用ObjectobjnewObject();// 强引用只要强引用还存在垃圾收集器永远不会回收被引用的对象——哪怕 OOM 也不会。这是 JVM 规范的硬性保证也是其他三种引用的对比基准。工程上绝大多数内存泄漏都源于本应是临时引用的强引用被意外长期持有比如publicclassLeakyCache{// 强引用持有永不回收privatestaticfinalMapString,byte[]CACHEnewHashMap();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 */publicclassSoftCacheK,V{privatefinalMapK,SoftReferenceVcachenewHashMap();publicvoidput(Kkey,Vvalue){cache.put(key,newSoftReference(value));}publicVget(Kkey){SoftReferenceVrefcache.get(key);returnrefnull?null:ref.get();}}SoftReference.get()在对象被回收后返回null调用方需要处理这种缓存未命中的情况。回收时机HotSpot 对软引用的回收遵循最近最少使用原则最近被访问过的软引用会保留更久。这是通过-XX:SoftRefLRUPolicyMSPerMB默认 1000ms参数控制的堆中每剩余 1MB 空间软引用的存活时间增加约 1 秒。# 调大软引用存活时长每 MB 多活 2 秒java-XX:SoftRefLRUPolicyMSPerMB2000-cpMyApp com.example.Main需要注意G1 之后默认开启-XX:UseG1GC软引用回收策略略有变化G1 通过-XX:G1SoftRefLRUPolicyMSPerMB单独控制。SoftReference 的代价软引用对象本身SoftReference实例是强引用它内部通过一个referent字段弱指向真正的对象。GC 在回收 referent 前会先检查软引用条件满足则把 referent 字段置 null、并把 SoftReference 实例放入关联的 ReferenceQueue。这意味着软引用本身仍占内存需要配合队列清理。WeakReference下次 GC 即回收WeakReference 比 SoftReference 更弱——只要发生 GC无论内存是否充足弱引用指向的对象都会被回收前提是没有其他强引用。典型应用WeakHashMapimportjava.util.WeakHashMap;classUserHolder{privatestaticfinalWeakHashMapObject,StringMETAnewWeakHashMap();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 源码片段staticclassEntryextendsWeakReferenceThreadLocal?{Objectvalue;Entry(ThreadLocal?k,Objectv){super(k);// key 是弱引用valuev;// 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{privatestaticfinalCleanerCLEANERCleaner.create();privatestaticclassStateimplementsRunnable{privatelongnativeHandle;// 模拟本地句柄State(longhandle){this.nativeHandlehandle;}Overridepublicvoidrun(){// 对象被回收时回调if(nativeHandle!0){freeNative(nativeHandle);nativeHandle0;}}}privatefinalStatestate;privatefinalCleaner.Cleanablecleanable;publicNativeResource(longhandle){this.statenewState(handle);this.cleanableCLEANER.register(this,state);}Overridepublicvoidclose(){cleanable.clean();// 主动清理}privatestaticnativevoidfreeNative(longhandle);}关键点State不持有外部对象NativeResource的引用避免阻止其回收。Cleaner.register(this, state)内部把this用虚引用包装、关联到 Cleaner 自己的队列。NativeResource被回收时Cleaner 线程会执行state.run()完成清理。也可以主动调用cleanable.clean()提前清理幂等。PhantomReference vs finalizefinalize()在 JDK 9 被标记 deprecatedJDK 18 起默认禁用。它的核心问题是可以复活对象、执行时机不确定、执行线程无保证。Cleaner 通过独立的清理线程和不可访问 referent的设计规避了这些问题。完整对比示例下面用一段代码同时演示四种引用在 GC 前后的行为差异importjava.lang.ref.*;importjava.util.ArrayList;importjava.util.List;publicclassReferenceDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ReferenceQueueObjectqueuenewReferenceQueue();// 1. 强引用ObjectstrongnewObject();// 2. 软引用SoftReferenceObjectsoftnewSoftReference(newObject(),queue);// 3. 弱引用WeakReferenceObjectweaknewWeakReference(newObject(),queue);// 4. 虚引用PhantomReferenceObjectphantomnewPhantomReference(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((refqueue.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包裹或改用ConcurrentHashMapWeakReference自行实现。3. ReferenceQueue 必须主动消费如果关联了队列但从不poll队列中的 Reference 对象会一直占用内存——这是隐性泄漏。Cleaner 内部已经处理了这个问题所以优先用 Cleaner。4. 虚引用的 referent 在入队前已被回收注意JDK 实现中PhantomReference 是在 referent 被** finalize 后、内存释放前**入队的。这意味着此时 referent 已无法访问但仍占用内存直到 Cleaner 执行完。所以虚引用不能用于复活对象。5. 软引用在 G1/ZGC 下的行为G1 对软引用采用分区回收策略可能延迟回收部分软引用ZGC 则在并发标记阶段处理。生产中如果发现软引用占用偏高可调整SoftRefLRUPolicyMSPerMB或改用显式 TTL 缓存。小结强引用永不回收是日常引用的默认形态也是泄漏主因。软引用在 OOM 前回收适合内存敏感的缓存存活时长由SoftRefLRUPolicyMSPerMB控制。弱引用下次 GC 即回收WeakHashMap与ThreadLocal都基于它。虚引用无法获取对象仅在对象回收前通过 ReferenceQueue 通知配合Cleaner替代finalize()。ReferenceQueue 是引用与回收通知的桥梁必须主动消费。下一篇我们将从判定对象存活过渡到如何回收深入对比四种经典垃圾回收算法。更多内容JVM调优实战