一、为什么需要不同强度的引用
写一个图片缓存:加载过的图片放进 Map,下次直接取。但如果缓存越积越大,内存吃紧了怎么办?你希望 GC 在内存不够时自动回收缓存里那些”暂时不用”的图片——可一旦你拿着强引用指向它们,GC 绝对不会动。
这就是矛盾:强引用太强,不允许回收;但有些场景恰恰需要”允许回收的引用”。
典型需求包括:
- 缓存:内存充裕时保留,内存紧张时让 GC 回收
- 规范映射(Canonicalizing Map):用 WeakHashMap 关联对象的元数据,对象本身被回收后映射条目自动消失
- 资源清理:对象被 GC 回收之前执行清理逻辑(关闭文件句柄、释放 native 内存)
一句话:我们需要分级的引用强度——从”绝对不回收”到”随时可回收”再到”回收前通知我”,逐级递减。
二、现实类比
机场登机有三种乘客:VIP 乘客持有头等舱登机牌,无论如何都能上飞机;普通乘客持有经济舱票,正常情况下能上,但航班超售时可能被挤掉;候补乘客只有等待名单上的名字,有空座才轮到,没座就算了。
酒店也一样:已入住的客人(强引用),房间不会被收回;预订了但可以超售让步的客人(软引用),酒店满房时可能被通知另找住处;等候名单上的人(弱引用),只是个名字,有房才联系你,没房你也不存在。
对应到引用类型:强引用是 VIP,软引用是普通乘客,弱引用是候补,虚引用是退房后帮你收拾房间的人。
三、核心思想
引用强度形成一条清晰的谱系:Strong > Soft > Weak > Phantom。每种强度对应 GC 不同的处理策略。
3.1 强引用(Strong Reference)
强引用是默认行为。只要有一条强引用链从 GC Roots 到达对象,这个对象就绝对不会被回收。即使是内存溢出(OOM),JVM 也不会释放强引用对象。
Object obj = new Object(); // 强引用obj = null; // 只有断开强引用,对象才可能被回收3.2 软引用(Soft Reference)
软引用在内存充裕时保留对象,内存不足时才由 GC 回收。JVM 保证在抛出 OOM 之前清除所有软可达对象。它是最适合做缓存的引用类型。
import java.lang.ref.SoftReference;
// 创建软引用SoftReference<byte[]> cache = new SoftReference<>(new byte[1024 * 1024]);
// 使用时需要检查是否已被回收byte[] data = cache.get();if (data == null) { // 已被 GC 回收,需要重新加载 data = new byte[1024 * 1024]; cache = new SoftReference<>(data);}3.3 弱引用(Weak Reference)
弱引用比软引用更弱——下一次 GC 运行时就会被清除,不管内存是否紧张。典型用途是规范映射,比如 Java 的 WeakHashMap:键是弱引用,键对象被回收后整个条目自动移除。
import java.lang.ref.WeakReference;
WeakReference<Object> ref = new WeakReference<>(new Object());
System.gc(); // GC 之后 ref.get() 大概率返回 nullSystem.out.println(ref.get()); // null3.4 虚引用(Phantom Reference)
虚引用是最弱的引用——get() 永远返回 null,你无法通过虚引用访问对象。它的唯一作用是:对象被 GC 确定回收后,虚引用会被加入引用队列(ReferenceQueue),让你在对象真正消失前执行清理逻辑。
虚引用 vs 终结器(finalizer):终结器的问题是对象可以在 finalize() 中”复活”自己,而虚引用无法复活对象,更安全。
import java.lang.ref.PhantomReference;import java.lang.ref.ReferenceQueue;
ReferenceQueue<Object> queue = new ReferenceQueue<>();PhantomReference<Object> ref = new PhantomReference<>(new Object(), queue);
// 永远返回 nullSystem.out.println(ref.get()); // null
// 通过队列检测回收Reference<?> r = queue.poll();if (r != null) { // 对象已被 GC,执行清理 cleanup();}3.5 引用队列(ReferenceQueue)
ReferenceQueue 是引用被 GC 清除后的通知机制。软引用、弱引用、虚引用都可以关联一个队列。GC 清除引用后,引用对象本身会被入队,你可以从队列中取出它并执行善后操作。
3.6 JVM 可达性层级
JVM 定义了五级可达性,GC 据此决定对象的生死:
| 可达性级别 | 含义 | GC 行为 |
|---|---|---|
| 强可达(Strongly Reachable) | 存在从 GC Roots 的强引用链 | 不回收 |
| 软可达(Softly Reachable) | 非强可达,但存在软引用 | 内存不足时回收 |
| 弱可达(Weakly Reachable) | 非软可达,但存在弱引用 | 下次 GC 时回收 |
| 虚可达(Phantomly Reachable) | 非弱可达,但存在虚引用 | 回收后入队通知 |
| 不可达(Unreachable) | 无任何引用 | 可最终回收 |
四、变体与对比
4.1 Java:完整的四级引用体系
Java 拥有最完整的引用强度体系:Strong、Soft、Weak、Phantom 四级,配合 ReferenceQueue 实现回收通知。WeakHashMap 是弱引用的经典应用——键为弱引用,值随键的回收而自动清除。
4.2 Go:runtime.SetFinalizer
Go 没有软引用和弱引用,最接近的是 runtime.SetFinalizer,它为对象注册一个在 GC 回收对象时调用的函数,功能上类似于虚引用的清理通知。
// 注册终结器:obj 被 GC 回收时调用 cleanupruntime.SetFinalizer(obj, func(o *MyResource) { o.Close() // 释放 native 资源})SetFinalizer 的局限:终结器执行顺序不确定,且对象可能在终结器中被意外复活。Go 社区普遍建议用 defer 和显式 Close() 替代,把 SetFinalizer 当安全网而非主要机制。
4.3 JavaScript / TypeScript:WeakRef + FinalizationRegistry
ES2021 引入了 WeakRef(弱引用)和 FinalizationRegistry(类似虚引用的清理通知)。这是 JavaScript 第一次暴露引用强度控制给开发者。
const cache = new Map();const registry = new FinalizationRegistry((key) => { cache.delete(key); // 对象被回收后清理缓存条目});
function getCached(key, loader) { if (cache.has(key)) { const ref = cache.get(key); const value = ref.deref(); // 检查弱引用是否还有效 if (value !== undefined) return value; } const value = loader(); cache.set(key, new WeakRef(value)); registry.register(value, key); // 注册清理回调 return value;}4.4 Python:weakref 模块
Python 提供 weakref.ref(弱引用)、weakref.WeakKeyDictionary 和 weakref.WeakValueDictionary。没有软引用和虚引用——Python 的引用计数 + GC 组合使得软引用的意义不大。
4.5 C#:WeakReference 与 ConditionalWeakTable
C# 的 WeakReference 分短周期(Short)和长周期(Long)两种。更有趣的是 ConditionalWeakTable<TKey, TValue>——它相当于一个键为弱引用的字典,但值会随键的回收而自动释放,本质上是弱引用 + 终结器的组合。
4.6 WeakHashMap vs ConcurrentHashMap + WeakReference
WeakHashMap 的键是弱引用,适合临时元数据关联。但它不是线程安全的。需要并发时,用 ConcurrentHashMap 配合 WeakReference 值,或者用 Guava 的缓存构建器指定弱引用策略。
| 方案 | 线程安全 | 键/值引用 | 适用场景 |
|---|---|---|---|
WeakHashMap | 否 | 键弱引用 | 单线程元数据关联 |
ConcurrentHashMap<K, WeakReference<V>> | 是 | 值弱引用 | 并发缓存 |
| Guava Cache + weakKeys/weakValues | 是 | 可配置 | 生产级缓存 |
五、多语言实现
5.1 Go:用 finalizer 模式实现缓存
Go 没有软引用和弱引用,但可以用 runtime.SetFinalizer 配合手动缓存淘汰来模拟类似效果。
package weakcache
import ( "runtime" "sync")
// Cache 基于 finalizer 的简易缓存type Cache[K comparable, V any] struct { mu sync.RWMutex items map[K]*entry[V]}
type entry[V any] struct { value V alive bool}
// New 创建缓存func New[K comparable, V any]() *Cache[K, V] { return &Cache[K, V]{ items: make(map[K]*entry[V]), }}
// Set 添加缓存项,注册 finalizer 作为安全网func (c *Cache[K, V]) Set(key K, value V) { c.mu.Lock() defer c.mu.Unlock()
e := &entry[V]{value: value, alive: true} c.items[key] = e
// finalizer 作为安全网:entry 不再被缓存引用时打印日志 // 注意:finalizer 不能替代显式清理,只是兜底 runtime.SetFinalizer(e, func(ee *entry[V]) { if ee.alive { // 如果还在 alive 状态被 GC 回收,说明缓存被意外清理 // 生产环境应记录监控指标 } })}
// Get 获取缓存项func (c *Cache[K, V]) Get(key K) (V, bool) { c.mu.RLock() defer c.mu.RUnlock()
e, ok := c.items[key] if !ok || !e.alive { var zero V return zero, false } return e.value, true}
// Remove 显式移除缓存项func (c *Cache[K, V]) Remove(key K) { c.mu.Lock() defer c.mu.Unlock()
if e, ok := c.items[key]; ok { e.alive = false runtime.SetFinalizer(e, nil) // 取消 finalizer delete(c.items, key) }}Go 的 runtime.SetFinalizer 有明显局限:执行时机不确定、不保证执行顺序、可能延迟对象的回收周期。上面的代码用 finalizer 做安全网,实际的缓存淘汰仍然依赖显式调用 Remove 或 LRU 策略。如果你的场景真正需要弱引用语义,考虑用 sync.Map + 定期清理来模拟。
5.2 TypeScript:WeakRef + FinalizationRegistry 实现对象缓存
class ObjectCache<K, V extends object> { private cache = new Map<K, WeakRef<V>>(); private registry: FinalizationRegistry<K>;
constructor() { // 对象被 GC 回收时自动清理缓存条目 this.registry = new FinalizationRegistry((key: K) => { const ref = this.cache.get(key); // 二次检查:对象可能已被重新缓存 if (ref && ref.deref() === undefined) { this.cache.delete(key); } }); }
// 获取缓存对象 get(key: K, loader: () => V): V { const ref = this.cache.get(key); if (ref) { const value = ref.deref(); if (value !== undefined) return value; // 弱引用已失效,清理残留条目 this.cache.delete(key); }
// 加载并缓存 const value = loader(); this.cache.set(key, new WeakRef(value)); this.registry.register(value, key); return value; }
// 手动移除 delete(key: K): void { this.cache.delete(key); }}使用示例:
interface ImageResource { width: number; height: number; data: Uint8Array;}
const imageCache = new ObjectCache<string, ImageResource>();
// 首次加载const img1 = imageCache.get("hero.png", () => loadImage("hero.png"));
// 后续获取直接返回缓存const img2 = imageCache.get("hero.png", () => loadImage("hero.png"));// img1 === img2,同一对象
// GC 回收后再次获取会重新加载// (需要在没有其他强引用指向该 ImageResource 时)FinalizationRegistry 的回调时机由 GC 决定,不能依赖它来做关键资源的确定性清理。对于文件句柄、网络连接等资源,仍然应该用 try/finally 或显式的 dispose() 模式。
六、生产验证
| 项目 | 源码位置 | 用途 |
|---|---|---|
| Guava Cache | guava/Caffeine | CacheBuilder.weakKeys() / softValues() 允许用弱引用/软引用作为缓存策略。键用弱引用时,键对象被 GC 回收后条目自动失效 |
| Python weakref | cpython/weakref.py | Python 标准库的弱引用实现。WeakKeyDictionary 用于给对象附加元数据而不阻止对象被回收 |
| V8 WeakRef | V8 WeakRef 实现 | V8 引擎对 ES2021 WeakRef 和 FinalizationRegistry 的实现。弱引用的清除发生在 GC 的标记阶段 |
七、小结
何时使用软引用:
- 内存敏感的缓存——图片、解码数据、计算结果等可以重建的对象
- 需要在内存不足时自动退让的场景,但充裕时尽量保留
何时使用弱引用:
- 规范映射——
WeakHashMap、WeakKeyDictionary,给对象附加元数据但不阻止回收 - 打破循环引用——Rust 的
Weak<T>、Python 的weakref.ref - 监听器/回调列表——避免因注册回调而阻止对象被回收
何时使用虚引用:
- 替代
finalize()做资源清理——更安全,无法复活对象 - 需要知道对象何时被 GC 回收,以便清理关联的外部资源
何时不用:
- 需要确定性清理的关键资源——用
try/finally、defer、RAII 模式 - 对缓存命中率有严格要求的场景——弱引用/软引用的回收时机不可控,命中率波动大
- Go 项目中过度依赖
runtime.SetFinalizer——执行时机和顺序都不确定
八、参考资料
- Java Reference 文档 - Java 四级引用体系的官方说明
- V8 WeakRef 与 FinalizationRegistry - V8 引擎对 ES2021 弱引用的实现细节
- Python weakref 文档 - Python 弱引用模块的使用指南
- Go runtime.SetFinalizer 文档 - Go 终结器机制的限制与用法
- Guava Cache 解释 - Guava 缓存库对弱引用/软引用策略的说明
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






