Java 四种引用类型:强引用、软引用、弱引用与虚引用
Java 四种引用类型强引用、软引用、弱引用与虚引用目录引用与可达性强引用软引用弱引用虚引用四种引用对比小结上一篇讲 ThreadLocal 时我们提到 Entry 的 key 使用了弱引用GC 可以在外部没有强引用时回收 ThreadLocal 对象。当时留了一个问题为什么 Entry 的 key 不能用强引用为什么 Java 要设计不同强度的引用普通引用会阻止对象被回收但缓存、附属数据映射和堆外资源清理并不总希望对象一直存活。为此Java 在普通强引用之外提供了软引用、弱引用和虚引用。引用与可达性Java 中对象是否可被回收取决于它从 GC Roots 出发是否仍然可达。但 Java 在 1.2 之后把引用分成了四个级别它们对对象的可达性影响不同引用类型对象可达状态GC 行为典型用途强引用强可达只要保持强可达就不会回收普通业务对象软引用仅软可达根据内存需求决定是否清除内存敏感数据弱引用仅弱可达GC 发现后会清除弱引用WeakHashMap、ThreadLocal key虚引用虚可达不提供对象访问清除后可入队native 资源清理通知强度越低GC 回收越不犹豫是通俗说法。严格来讲四种引用对应四种可达状态GC 对每种状态有不同的处理策略。强引用强引用是最常见的引用方式我们平时写的代码几乎全是强引用Stringname张三;ListStringlistnewArrayList();ObjectobjnewObject();只要强引用还在GC 永远不会回收对应的对象。即使内存不足JVM 宁可抛出OutOfMemoryError也不会去回收一个还有强引用指向的对象。ObjectobjnewObject();// obj 强引用了 Object 实例Objectrefobj;// ref 也强引用了同一个实例objnull;// obj 断开了引用// 但 ref 还在GC 不会回收这个 Object 实例refnull;// ref 也断开了// 此时没有任何强引用指向该实例GC 可以回收它对象能否继续保持强可达取决于程序中的引用关系对象具体何时被回收仍由 GC 决定。如果对象被静态变量、集合、线程或其他长生命周期对象意外持有它就会一直保持强可达形成内存泄漏。大多数情况下强引用就是你需要的。只有在内存敏感缓存、附属数据映射以及 native 资源清理这些场景下才需要用到下面三种引用。软引用软引用用SoftReference类实现。当 GC 判断某个对象仅被软引用可达时会根据当前内存需求决定是否清除该引用。内存充足时保留内存不足时回收。importjava.lang.ref.SoftReference;// 创建一个软引用指向一个大数组SoftReferencebyte[]softRefnewSoftReference(newbyte[1024*1024]);// 通过软引用获取对象byte[]datasoftRef.get();if(data!null){// 对象还在正常使用System.out.println(数据长度: data.length);}else{// GC 已清除软引用需要重新加载dataloadFromDisk();}softRef.get()是关键方法。如果对象还没有被回收返回对象本身如果已经被回收返回null。软引用与缓存软引用最典型的应用场景是内存敏感的缓存。假设你有一个图片缓存系统图片加载很慢你希望尽量把图片留在内存里但如果内存不够了应该让 GC 自动回收这些图片而不是 OOM。publicclassImageCache{privatefinalMapString,SoftReferencebyte[]cachenewHashMap();publicbyte[]getImage(Stringurl){SoftReferencebyte[]refcache.get(url);if(ref!null){byte[]dataref.get();if(data!null){returndata;// 缓存命中}}// 缓存未命中或已被回收重新加载byte[]datadownloadImage(url);cache.put(url,newSoftReference(data));returndata;}}这个缓存的特性是内存够用就尽量留着内存紧张时 GC 会清除仅软可达的对象。不过要注意两个问题。第一软引用的回收时机由 GC 决定不同 JVM 对内存不足的判定标准不同不能假设软引用一定在某个内存阈值被回收。官方只保证在抛出OutOfMemoryError之前会清除所有仅软可达的对象。第二软引用被清除后HashMap 中对应的 key 和SoftReference包装对象不会自动删除。长期运行的缓存还需要定期清理失效 Entry或者配合ReferenceQueue处理。因此软引用可以实现简单的内存敏感缓存但回收时机不可预测命中率和性能不容易稳定。实际项目如果需要稳定的容量、过期时间和淘汰顺序通常更适合使用显式缓存策略如 Caffeine、Guava Cache。弱引用ThreadLocal 的 key 为什么不能使用强引用现在就能解释了。弱引用用WeakReference类实现。当 GC 发现一个对象仅被弱引用可达时会清除指向它的弱引用。之后该对象便可以被回收。importjava.lang.ref.WeakReference;ObjectobjnewObject();WeakReferenceObjectweakRefnewWeakReference(obj);// 断开最后一个强引用objnull;System.gc();// 建议 JVM 执行 GC不保证立即发生// 如果 GC 已经执行并发现对象仅弱可达弱引用已被清除System.out.println(weakRef.get());// 可能为 null注意System.gc()只是建议 JVM 执行 GC并不构成确定性测试。weakRef.get()返回null意味着 GC 已经发现该对象仅弱可达并清除了弱引用但具体何时发生取决于 GC 实现。ThreadLocal 的弱引用设计回到 ThreadLocal。Entry 的 key 使用弱引用staticclassEntryextendsWeakReferenceThreadLocal?{Objectvalue;Entry(ThreadLocal?key,Objectvalue){super(key);// key 是弱引用this.valuevalue;}}如果 Entry 对 ThreadLocal 使用强引用即使业务代码已经不再使用这个 ThreadLocal只要线程仍然存活ThreadLocal 对象及其 value 就始终无法回收。弱引用解除了 Map 对 ThreadLocal 生命周期的强绑定。当外部代码不再持有 ThreadLocal 对象时GC 发现它仅弱可达会清除 Entry 的 key。ThreadLocalMap 在后续操作中发现 key 为 null 的 Entry会清理对应的 value。弱引用不能代替主动清理但能避免问题进一步扩大。WeakHashMapJDK 提供了一个基于弱引用的 Map 实现WeakHashMap。它的 key 是弱引用当 key 对象失去外部强引用后GC 发现 key 仅弱可达时会清除对应的 Entry。importjava.util.WeakHashMap;WeakHashMapUser,StringmapnewWeakHashMap();UserusernewUser(张三);map.put(user,管理员);// user 还在Entry 存活System.out.println(map.get(user));// 管理员usernull;// 断开强引用System.gc();// 如果 GC 已执行Entry 可能已被清除System.out.println(map.size());// GC 完成后可能变为 0WeakHashMap 适合做附属数据的存储。比如你想给某些对象附加额外信息但不想因为附加数据而延长这些对象的生命周期。对象没有其他引用时附加数据自动清理。需要注意一个陷阱WeakHashMap 的 value 仍然是强引用。如果 value 直接或间接引用了自己的 keykey 仍然强可达弱引用就失去作用了WeakHashMapUser,UserProfilemapnewWeakHashMap();UserusernewUser(张三);UserProfileprofilenewUserProfile(user);// value 强引用了 keymap.put(user,profile);usernull;// 存在引用链WeakHashMap → value(profile) → key(user)// user 仍然强可达Entry 不会被 GC 清除这是官方文档专门提醒的一个陷阱。如果 value 会引用 key弱 key 的设计就白费了。虚引用虚引用用PhantomReference类实现是最弱的一种引用。它有一个特殊性质get()永远返回null不能通过虚引用访问对象本身。importjava.lang.ref.PhantomReference;importjava.lang.ref.ReferenceQueue;ReferenceQueueObjectqueuenewReferenceQueue();ObjecttargetnewObject();PhantomReferenceObjectphantomRefnewPhantomReference(target,queue);ObjectobjphantomRef.get();// 永远返回 null为什么get()要设计成永远返回null因为虚引用的唯一作用是感知对象的回收状态而不是访问对象。如果get()能返回对象程序就可以在 GC 准备回收时重新建立强引用阻止回收这会破坏 GC 的语义。配合 ReferenceQueue 做回收通知虚引用通常配合ReferenceQueue使用。当 GC 判断对象已经进入虚可达状态时会清除相关虚引用并在随后将其加入关联的ReferenceQueue。程序可以监听队列执行资源清理等操作。ReferenceQueuebyte[]queuenewReferenceQueue();SetPhantomReferencebyte[]referencesnewHashSet();byte[]datanewbyte[1024];PhantomReferencebyte[]phantomnewPhantomReference(data,queue);references.add(phantom);// 必须保持对虚引用本身的强引用datanull;// 断开对目标对象的强引用// 后台线程持续监听队列newThread(()-{while(!Thread.currentThread().isInterrupted()){try{Reference?extendsbyte[]refqueue.remove();System.out.println(对象已进入虚可达状态执行资源清理);references.remove(ref);ref.clear();}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}}).start();注意两个细节。第一虚引用对象本身也必须被强引用持有这里用 Set 保存否则它可能在入队前就失去引用程序无法可靠地从队列中获得它。第二监听线程用循环而不是单次remove()因为队列中可能不断有新的虚引用入队。DirectByteBuffer 的堆外内存清理虚引用最经典的应用是 Java NIO 中的DirectByteBuffer。DirectByteBuffer分配的是堆外内存native memory不受 JVM GC 直接管理。即使DirectByteBuffer对象被 GC 回收了它分配的堆外内存不会自动释放需要显式调用Unsafe.freeMemory()。在 OpenJDK 的实现中DirectByteBuffer会为底层内存注册一个 Cleaner。Cleaner 基于虚引用和 ReferenceQueue 跟踪缓冲区对象当对象不再可达时执行清理任务释放对应的 native memory。这就是虚引用的核心价值在对象进入回收阶段时执行 GC 无法自动完成的清理工作。GC 管的是堆内存堆外的 native 资源需要这种额外的通知机制。四种引用对比维度强引用软引用弱引用虚引用类型普通赋值SoftReferenceWeakReferencePhantomReference可达状态强可达仅软可达仅弱可达虚可达GC 行为不回收内存不足时清除发现后清除清除后可入队get()返回对象本身对象或 null对象或 null永远 null能否阻止回收能内存充足时能不能不能典型场景普通业务对象内存敏感数据ThreadLocal、WeakHashMapnative 资源清理可配合 ReferenceQueue可以可以可以通常需要小结四种引用的区别不是语法不同而是它们对对象可达性的影响不同。强引用决定了绝大多数业务对象的生命周期软引用和弱引用允许 GC 在特定条件下清除对象虚引用不提供对象访问主要用于感知对象进入回收阶段并清理关联资源。实际开发中强引用仍然是默认选择。弱引用适合不应延长 key 生命周期的映射关系典型如 ThreadLocal 的 Entry key。虚引用适合处理堆外内存等资源清理。软引用虽然可以构造内存敏感缓存但回收策略不可预测生产缓存通常更适合使用显式容量和过期规则。