WeakHashMap 的键回收不实时,依赖 GC 后的 expungeStaleEntries() 调用(如 get/size/entrySet),若只写不读则失效条目滞留;值为强引用,需自行管理生命周期,否则易致内存泄漏;并发与迭代行为不稳定,生产环境推荐 Guava Cache 或 Caffeine。

WeakHashMap 的键回收时机不等于 GC 触发时刻
WeakHashMap 不会在你调用 put() 或 get() 后立刻清理“已无强引用”的键,而是依赖垃圾回收器扫描后触发 expungeStaleEntries() —— 这个方法只在部分操作中被调用,比如 size()、get()、put()、entrySet() 等。它不是实时的,也不是定时的。
这意味着:如果你只往 WeakHashMap 里塞数据,但从不读、不查、不遍历,那些本该失效的条目就一直挂着,直到某次访问才被顺手清理。实际缓存场景中,这容易造成「内存已空但缓存不缩容」的假象。
- 务必在关键路径(如
get())后主动触发一次size()或isEmpty(),确保 stale entry 被清理 - 不要依赖
System.gc()—— 它只是建议,JVM 可能忽略;且现代 GC(如 ZGC、Shenandoah)对弱引用处理逻辑更复杂,手动触发反而干扰节奏 - 如果缓存命中率低、写多读少,考虑包装一层带周期性
expungeStaleEntries()调用的代理类(通过反射调用私有方法或继承重写)
值对象必须自己管理生命周期,WeakHashMap 不管它
WeakHashMap 只对键做弱引用,值是强引用。一旦键被回收,对应 Entry 被移除,但值对象若还被其他地方持有(比如缓存结果被返回后又被上层长期持有),它就不会被 GC —— 这正是最隐蔽的内存泄漏源头。
典型踩坑场景:缓存一个大 byte[] 或 BufferedImage,键是查询参数对象(很快丢弃),但返回值被 UI 层强引用着,导致值对象永久驻留。
- 值类型尽量用不可变小对象(如
String、Integer),避免大对象直存 - 若必须缓存大对象,考虑用
SoftReference<v></v>包一层再存为值(即WeakHashMap<k softreference>></k>),让值也能响应内存压力 - 绝不要把线程局部变量、监听器、回调函数等生命周期不确定的对象作为值存进去
不能当普通 Map 用:并发、迭代、size 都不稳定
由于键可能在任意 GC 后消失,WeakHashMap 的行为天然非确定。连续两次 get(key) 可能一次返回值、一次返回 null;size() 在两次调用间可能突变;遍历 entrySet() 时甚至可能抛 ConcurrentModificationException(虽然它没实现 ConcurrentMap,但弱引用清理本身就有隐式修改)。
它不适合做状态一致要求高的缓存,比如分布式 session 映射或事务上下文存储。
- 禁止在 for-each 循环中直接修改缓存(如边遍历边
remove()),改用Iterator.remove() - 高并发场景下,用
Collections.synchronizedMap(new WeakHashMap())仅解决线程安全问题,不解决语义漂移;更稳妥的是用ConcurrentHashMap+ 定时清理策略 - 如果业务需要稳定 size 或可预测的迭代顺序,WeakHashMap 就不该是首选
替代方案比硬套 WeakHashMap 更常见
真正生产环境的轻量本地缓存,90% 以上不会直接裸用 WeakHashMap。它太原始,边界模糊,调试困难。主流做法是组合使用:
- Guava Cache:支持 weakKeys() / weakValues() / softValues(),自动清理 + 过期 + 并发控制 + 统计,一行配置搞定
- Caffeine:性能更强,基于 Window TinyLFU 算法,weak/soft 支持更精细,且提供
onRemoval()回调便于诊断 - 自研简易缓存:用
ConcurrentHashMap存数据,另起一个低优先级线程定期扫描 key 引用状态(配合ReferenceQueue),可控性更高
WeakHashMap 最适合的场景其实很窄:临时、短命、低一致性要求、且键对象本身生命周期明确可控(比如 Swing 中监听器与组件的映射)。把它当成“自动 GC 友好型 Map”可以,当成“开箱即用缓存”就会反复踩坑。

















