一、什么是 WeakHashMap?
WeakHashMap<K, V> 是 Java 标准库(java.util 包)中的一种特殊 Map 实现。它的核心特性是:
键(Key)被包装为弱引用(WeakReference) ,当某个键对象 不再被任何强引用持有 时,即使它还在
WeakHashMap中,也会在下一次垃圾回收(GC)时被自动移除。
这使得 WeakHashMap 成为一种 自动清理的 Map,非常适合用于缓存或元数据映射等场景。
二、WeakHashMap 的基本使用
示例代码:观察自动清理行为
java
import java.util.WeakHashMap;
public class WeakHashMapDemo {
public static void main(String[] args) {
WeakHashMap<Key, String> map = new WeakHashMap<>();
Key key = new Key("obj1");
map.put(key, "value1");
System.out.println("Before GC: " + map.size()); // 输出 1
key = null; // 移除唯一强引用
// 建议触发 GC(不保证立即执行)
System.gc();
try { Thread.sleep(100); } catch (InterruptedException e) {}
// 调用 size() 会触发 expungeStaleEntries()
System.out.println("After GC: " + map.size()); // 通常输出 0
}
static class Key {
private final String name;
public Key(String name) { this.name = name; }
@Override
public String toString() { return name; }
}
}
⚠️ 注意:
System.gc()只是建议 JVM 执行 GC,实际是否回收取决于 JVM 实现和运行时状态。但只要 GC 发生,且 key 无强引用,条目就会被清除。
三、适用场景(Where to Use)
✅ 推荐场景:
-
对象元数据缓存(Metadata Cache)
例如:为每个对象关联一个调试信息、监听器列表、临时配置等,但不希望这些元数据阻止对象被回收。
-
避免内存泄漏的监听器注册表
在事件驱动系统中,若监听器未显式注销,可能造成内存泄漏。使用
WeakHashMap<Listener, ?>可让监听器在无其他引用时自动移除。 -
ClassLoader 或 Class 相关的缓存
比如动态代理类、反射元数据缓存等,避免因缓存导致 ClassLoader 无法卸载。
❌ 不适合场景:
- 需要长期保留数据的缓存(如用户会话、热点数据)→ 应使用
SoftReference(软引用)或成熟缓存框架(如 Caffeine、Guava Cache)。 - 键是基本类型包装类(如
Integer,String)且来自常量池 → 因为它们可能被 JVM 强引用(如字符串字面量),导致无法回收。
四、WeakHashMap 的优缺点
| 优点 | 缺点 |
|---|---|
| ✅ 自动清理无用条目,防止内存泄漏 | ❌ 条目可能随时消失,不适合需要稳定存储的场景 |
| ✅ 与 GC 协同工作,无需手动管理 | ❌ 清理时机不可控(依赖 GC) |
| ✅ 内存友好,适合辅助性缓存 | ❌ 性能略低于 HashMap(每次操作需清理 stale entries) |
| ✅ 线程不安全(与 HashMap 一致) | ❌ 不支持 null key(因为 WeakReference(null) 无意义) |
补充:
WeakHashMap不允许null作为 key ,但允许null作为 value。
五、底层原理(How It Works)
1. 弱引用(WeakReference)
WeakHashMap 内部使用 WeakReference 包装每个 key:
java
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
// ...
}
- 当 key 对象仅被
WeakReference引用时,JVM 认为其"可回收"。 - GC 时,JVM 会将该
WeakReference加入其关联的ReferenceQueue。
2. ReferenceQueue 机制
WeakHashMap 构造时会创建一个 ReferenceQueue:
java
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
- 当 key 被 GC 回收后,对应的
Entry会被放入此队列。 WeakHashMap在 每次访问(get/put/size 等)时 ,会调用expungeStaleEntries()方法,遍历队列并删除已失效的条目。
这就是为什么
map.size()在 GC 后返回 0 ------ 因为size()触发了清理。
3. 清理不是实时的!
⚠️ 关键点 :WeakHashMap 不会在 GC 后立即清理,而是在下次调用其方法时才清理。因此:
- 如果长时间不访问 map,stale entries 会堆积(但不占用 key 对象内存,只占 Entry 对象内存)。
- 它不是"实时缓存",而是"懒清理"。
六、与 JVM 的关系
WeakHashMap 的行为高度依赖 JVM 的 垃圾回收机制 和 引用类型语义:
| 引用类型 | 是否阻止 GC | WeakHashMap 使用 |
|---|---|---|
| 强引用(Strong) | ✅ 是 | 不使用 |
| 软引用(Soft) | ✅(直到内存不足) | SoftReference,用于内存敏感缓存 |
| 弱引用(Weak) | ❌ 否 | WeakHashMap 的 key |
| 虚引用(Phantom) | ❌ 否 | 用于跟踪对象回收(不用于 Map) |
因此,
WeakHashMap的"自动清理"能力完全由 JVM 的 GC 决定,属于 JVM 与 Java 语言协同设计的典型范例。
七、生产环境是否使用?如何使用?
✅ 生产环境可以使用,但需谨慎!
正确使用姿势:
- 明确目的:仅用于"辅助性、可丢失"的元数据映射。
- 避免依赖清理时机:不要假设条目何时消失。
- 不要用于主业务数据存储。
- 注意 key 的生命周期:确保 key 确实会变成"仅被 WeakHashMap 引用"。
生产级示例:监听器注册表(防内存泄漏)
java
import java.util.*;
public class EventManager {
// 使用 WeakHashMap 避免监听器泄漏
private final Map<Listener, Boolean> listeners = new WeakHashMap<>();
public void addListener(Listener listener) {
listeners.put(listener, Boolean.TRUE);
}
public void fireEvent(String event) {
// 清理 stale entries 并通知有效监听器
for (Listener l : new ArrayList<>(listeners.keySet())) {
if (l != null) {
l.onEvent(event);
}
}
}
interface Listener {
void onEvent(String event);
}
}
即使调用方忘记
removeListener,只要监听器对象无其他强引用,就会被自动清理。
八、替代方案对比
| 方案 | 特点 | 适用场景 |
|---|---|---|
WeakHashMap |
key 弱引用,GC 后自动移除 | 元数据、监听器、临时关联 |
SoftReference + Map |
key 软引用,内存不足时才回收 | 内存敏感缓存(如图片缓存) |
| Guava Cache / Caffeine | 支持 LRU、TTL、软/弱引用等 | 通用缓存,功能强大 |
IdentityHashMap |
基于 == 比较 | 特殊场景(如防止重写 equals 的干扰) |
一般建议:除非明确需要弱引用语义,否则优先使用成熟缓存框架。
九、总结
| 维度 | 说明 |
|---|---|
| 本质 | key 为弱引用的 Map |
| 核心机制 | 依赖 JVM GC + ReferenceQueue |
| 自动清理 | 在下次访问时触发(懒清理) |
| 线程安全 | ❌ 不安全(需外部同步) |
| 生产可用 | ✅ 但仅限特定场景(元数据、防泄漏) |
| 与 JVM 关系 | 深度依赖 GC 行为和引用语义 |
💡 一句话口诀 :
"WeakHashMap 存的是'影子',主身一走,影子就散。"