weakhashmap 的 key 被 gc 回收是因为 entry 继承 weakreference,key 为弱引用而 value 不是;它不适合普通缓存,因 key 若无外部强引用则 gc 后 entry 即消失,且不支持过期、lru 等策略。

WeakHashMap 为什么不会阻止 key 被 GC 回收
关键在它的 Entry 继承自 WeakReference:key 是弱引用,value 不是。只要没有其他强引用指向该 key,下一次 GC 就可能把它连同对应 Entry 一起清理掉。
这决定了它**不能当普通 Map 用**——比如你 put 一个只在局部变量里存在的对象作 key,过几行代码后 get 就可能返回 null,不是 bug,是设计如此。
- key 必须是“外部还有强引用维持生命周期”的对象,比如 GUI 组件、Activity 实例、Spring Bean 等长期存活的实体
- value 如果持有对 key 的反向引用(比如 value 是个监听器且内部存了 this),会间接阻止 key 被回收,导致内存泄漏
- 不适用于 String、Integer 等常量池对象作 key——它们本就常驻堆,弱引用没意义,还可能因字符串驻留导致 Entry 意外残留
缓存场景下 WeakHashMap 的典型误用
很多人一看到“自动清理”就直接拿 WeakHashMap 当缓存用,结果发现缓存命中率极低,甚至刚 put 就 get 不到。
根本原因是:缓存 key 往往是临时构造的 DTO、查询参数对象,没被其他地方持有时,GC 一来就清空了。
- 错误示例:
cache.put(new UserQuery("id123"), user)→UserQuery实例无外部引用,下次 GC 后 Entry 消失 - 正确做法:key 应复用或由长期存活对象生成,比如用
user.getId()(String)作 key,但需注意 String 常量池影响;更稳妥的是用WeakHashMap配合ReferenceQueue做清理钩子,或改用SoftReference包装 value - 别指望它替代
Caffeine或Guava Cache:它不支持过期、大小限制、LRU 等缓存策略,纯靠 GC 触发,不可控
WeakHashMap 和 HashMap 在遍历时的行为差异
WeakHashMap 的迭代器是“弱一致”的:遍历过程中如果 key 被回收,对应 Entry 可能在 next() 时直接跳过,不会抛异常,但 size() 可能和实际 entrySet() 大小不一致。
这意味着你不能依赖 size() 判断缓存容量,也不能在 for-each 中修改 value 并期望后续逻辑读到新值——Entry 可能中途消失。
- 遍历时若需稳定访问,先用
entrySet().toArray()快照一份,再处理数组 -
keySet()、values()返回的集合也是弱一致的,同样可能漏项 - 多线程环境下必须额外同步,它本身不保证并发安全,和
HashMap一样
替代方案:什么时候该换别的实现
真要实现自动清理缓存,WeakHashMap 往往不是最优解。它太被动,清理时机完全交给 GC,而 GC 又受堆大小、GC 算法、JVM 参数影响极大。
- 需要可控过期:用
Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES) - 需要内存敏感但比弱引用更“懒”的回收:value 用
SoftReference包装,配合自定义 Map 实现 - 要监听清理事件:
WeakHashMap不提供回调,得自己维护ReferenceQueue+ 定期轮询,复杂度陡增 - Key 是基本类型或不可变对象:直接用
ConcurrentHashMap+ 定时任务清理过期项,更直观可靠
WeakHashMap 的价值不在缓存,而在“绑定生命周期”:比如给每个 Thread 存一份上下文,或为 GUI 控件挂载临时元数据——这些场景里 key 的存活周期天然明确,清理才真正自动且可预期。










