软引用缓存的核心难点是回收后联动清理key映射关系,必须搭配ReferenceQueue才能在GC回收当轮捕获软引用并触发清理;通过自定义SoftEntry持有key并关联队列,结合ConcurrentHashMap与主动轮询或后台线程实现自动清理。

软引用缓存的核心难点不是“存”,而是“被回收后如何联动清理 key 的映射关系”。单纯用 SoftReference 包裹 value,无法感知回收时机;而 ReferenceQueue 正是解决这一问题的关键——它能让你在 GC 回收软引用对象的**同一轮周期内**捕获该引用,并触发清理逻辑。
为什么必须搭配 ReferenceQueue?
软引用(SoftReference)本身是被动的:你无法主动判断它是否还有效,也不能监听“刚刚被清空”这个瞬间。JVM 在回收软引用时,会将其入队到关联的 ReferenceQueue 中(前提是构造时传入了 queue)。这提供了唯一可靠的、**异步但及时**的回收通知机制。
注意:入队发生在 GC 完成后、程序下次调用 queue.poll() 或 queue.remove() 时可见,不是实时回调,但对缓存清理完全够用。
设计一个带自动清理的 SoftReferenceMap
核心思路是:用 SoftReference 存 value,同时把 key 和该软引用绑定;当软引用入队,就根据绑定关系删掉 map 中对应的 key。
推荐结构:
- 使用
ConcurrentHashMap存主映射(线程安全,避免清理时并发修改异常)> - 维护一个独立的
ReferenceQueue,所有软引用都关联它 - 让软引用持有 key 的弱引用(或直接用
SoftReference的子类携带 key),避免因保存强引用 key 导致内存泄漏
关键实现:自定义 SoftEntry 捕获 key
标准 SoftReference 不带 key 信息,所以需扩展:
static class SoftEntry<k v> extends SoftReference<v> {
final K key;
SoftEntry(V referent, K key, ReferenceQueue<v> q) {
super(referent, q);
this.key = key;
}
}</v></v></k>
这样,当 SoftEntry 入队,就能通过 entry.key 精准定位要删除的 map 条目。
put 逻辑示例:
public void put(K key, V value) {
// 清理已失效引用(可选:定期调用,或在每次 put 前执行)
cleanStaleEntries();
map.put(key, new SoftEntry(value, key, queue));
}
清理线程 or 主动轮询?
两种常见策略:
-
后台守护线程:启动一个低优先级线程,循环
queue.remove()(阻塞直到有引用入队),拿到SoftEntry后map.remove(entry.key)。适合高吞吐、对延迟不敏感的场景 -
写操作前清理:每次
put/remove时,先poll()队列并清理一批(比如最多 10 个),避免队列堆积。更轻量,适合中小规模缓存
注意:不要在 get() 中清理——读多写少时会浪费 CPU;也不要依赖 finalize 或虚引用,它们语义不同且不可靠。
不复杂但容易忽略:软引用的生命周期受 JVM 内存压力影响,-XX:SoftRefLRUPolicyMSPerMB 参数会影响保留时间,生产环境建议监控 ReferenceQueue 积压情况,防止 key 泄漏。










