如何通过SoftReference构建内存敏感二级缓存,掌握其回收机制?
- 内容介绍
- 相关推荐
本文共计879个文字,预计阅读时间需要4分钟。
软引用适合作为内存敏感的二级行为缓存,但其回收并非内存一紧张就清除,而是由JVM结合堆空间空闲率、软引用使用率、长时间未使用和GC类型共同决定——关键在于理解其非主动、非实时、看时机的特性。
软引用的回收时机:不是 Full GC 就收,而是“看空闲堆+算闲置时间”
SoftReference 指向的对象不会在 Minor GC 中被处理,只有在触发 Full GC 且堆内存仍不足时,JVM 才会考虑回收。但是否真回收,还要看两个硬条件:
- 对象没有强引用链可达(即已脱离业务主流程)
- 该软引用的“闲置时间”超过阈值:clock − timestamp > heap_free_mb × -XX:SoftRefLRUPolicyMSPerMB(默认每 MB 空闲堆允许存活 1000ms)
例如:当前堆空闲 50MB,按默认策略,软引用最多可闲置 50 × 1000 = 50,000ms(50秒);若某缓存项 60 秒没被 get() 访问过,下次 Full GC 时就大概率被清掉。
用 SoftReference 构建二级缓存:结构要轻、访问要快、容错要稳
一个实用的 SoftReference 二级缓存核心结构包含三部分:
- 缓存容器:用 ConcurrentHashMap<String, SoftReference<Object>> 存键与软引用映射,避免 HashTable 的锁开销
- 取值逻辑:每次 get(key) 都需判空重载,因为 softRef.get() 可能返回 null
- 兜底机制:null 时触发加载并重建 SoftReference,不抛异常、不阻塞主线程
示例片段:
public <T> T get(String key, Supplier<T> loader) {SoftReference<T> ref = cache.get(key);
T obj = (ref != null) ? ref.get() : null;
if (obj == null) {
obj = loader.get();
cache.put(key, new SoftReference<>(obj));
}
return obj;
}
为什么它不适合作为生产级二级缓存?三个现实短板
SoftReference 缓存看似省心,但在真实场景中容易引发问题:
- 无业务语义:无法设置 TTL、LFU 或 LRU,淘汰完全依赖 JVM 内部估算,与访问热度脱钩
- 缓存击穿风险高:多个线程同时发现 softRef.get() == null,全部回源查库,瞬间压垮 DB
- 不可预测性:同一缓存项在压力低时存活数分钟,压力高时秒删;开发者无法感知或干预回收节奏
MyBatis 的 SOFT 缓存策略正是基于此实现,但它明确要求配合合理的 size 限制和刷新机制,单独依赖 SoftReference 并不足够。
比 SoftReference 更可控的替代思路
如果目标是真正可用的内存敏感缓存,建议组合使用:
- 用 WeakReference + 显式驱逐 做短期快照(如 UI 渲染中间态),回收时机明确(下次 GC 即清)
- 用 Caffeine 或 Guava Cache 做主缓存:支持 access/weight-based 驱逐、异步加载、统计监控,生命周期由业务定义
- 把 SoftReference 当作“最后防线”:在 Caffeine 已满且内存告急时,再将部分冷数据降级为 SoftReference 存储,不作为主力路径
本文共计879个文字,预计阅读时间需要4分钟。
软引用适合作为内存敏感的二级行为缓存,但其回收并非内存一紧张就清除,而是由JVM结合堆空间空闲率、软引用使用率、长时间未使用和GC类型共同决定——关键在于理解其非主动、非实时、看时机的特性。
软引用的回收时机:不是 Full GC 就收,而是“看空闲堆+算闲置时间”
SoftReference 指向的对象不会在 Minor GC 中被处理,只有在触发 Full GC 且堆内存仍不足时,JVM 才会考虑回收。但是否真回收,还要看两个硬条件:
- 对象没有强引用链可达(即已脱离业务主流程)
- 该软引用的“闲置时间”超过阈值:clock − timestamp > heap_free_mb × -XX:SoftRefLRUPolicyMSPerMB(默认每 MB 空闲堆允许存活 1000ms)
例如:当前堆空闲 50MB,按默认策略,软引用最多可闲置 50 × 1000 = 50,000ms(50秒);若某缓存项 60 秒没被 get() 访问过,下次 Full GC 时就大概率被清掉。
用 SoftReference 构建二级缓存:结构要轻、访问要快、容错要稳
一个实用的 SoftReference 二级缓存核心结构包含三部分:
- 缓存容器:用 ConcurrentHashMap<String, SoftReference<Object>> 存键与软引用映射,避免 HashTable 的锁开销
- 取值逻辑:每次 get(key) 都需判空重载,因为 softRef.get() 可能返回 null
- 兜底机制:null 时触发加载并重建 SoftReference,不抛异常、不阻塞主线程
示例片段:
public <T> T get(String key, Supplier<T> loader) {SoftReference<T> ref = cache.get(key);
T obj = (ref != null) ? ref.get() : null;
if (obj == null) {
obj = loader.get();
cache.put(key, new SoftReference<>(obj));
}
return obj;
}
为什么它不适合作为生产级二级缓存?三个现实短板
SoftReference 缓存看似省心,但在真实场景中容易引发问题:
- 无业务语义:无法设置 TTL、LFU 或 LRU,淘汰完全依赖 JVM 内部估算,与访问热度脱钩
- 缓存击穿风险高:多个线程同时发现 softRef.get() == null,全部回源查库,瞬间压垮 DB
- 不可预测性:同一缓存项在压力低时存活数分钟,压力高时秒删;开发者无法感知或干预回收节奏
MyBatis 的 SOFT 缓存策略正是基于此实现,但它明确要求配合合理的 size 限制和刷新机制,单独依赖 SoftReference 并不足够。
比 SoftReference 更可控的替代思路
如果目标是真正可用的内存敏感缓存,建议组合使用:
- 用 WeakReference + 显式驱逐 做短期快照(如 UI 渲染中间态),回收时机明确(下次 GC 即清)
- 用 Caffeine 或 Guava Cache 做主缓存:支持 access/weight-based 驱逐、异步加载、统计监控,生命周期由业务定义
- 把 SoftReference 当作“最后防线”:在 Caffeine 已满且内存告急时,再将部分冷数据降级为 SoftReference 存储,不作为主力路径

