WeakHashMap 常被誤解為「自動清理無用快取的神器」,實際上其弱引用機制僅作用於 Key,且清理時機強依賴「主動訪問 Map」。本文從源碼層面剖析 WeakHashMap 與 HashMap 的本質差異,揭示「不訪問不清理」的記憶體洩漏陷阱,並給出正確使用姿勢。
| 特性 | HashMap | WeakHashMap |
|---|---|---|
| Key 引用型別 | 強引用 | 弱引用 (WeakReference<K>) |
| Entry 繼承體系 | Node<K,V> implements Map.Entry | Entry<K,V> extends WeakReference<K> |
| Key 回收條件 | Map 移除或 Map 不可達 | Key 僅被 WeakHashMap 參考(無強引用) |
| Value 引用 | 強引用 | 強引用(同 HashMap) |
| 清理觸發時機 | 無自動清理 | 調用 get/put/size/iterator 等方法時 |
| 線程安全 | 非線程安全 | 非線程安全 |
static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
Entry(K key, V value, ReferenceQueue<K> q, int h, Entry<K,V> n) {
super(key, q); // Key 註冊到 ReferenceQueue
this.value = value; // Value 強引用
this.hash = h;
this.next = n;
}
}
關鍵點:super(key, q) 將 Key 包裝為弱引用並登記到 ReferenceQueue;Value 直接強引用掛在 Entry 上。
private void expungeStaleEntries() {
for (Entry<K,V> e = queue.poll(); e != null; e = queue.poll()) {
int h = e.hash;
int i = indexFor(h, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e) table[i] = next;
else prev.next = next;
e.value = null; // 釋放 Value 強引用
size--;
break;
}
prev = p;
p = next;
}
}
}
此方法遍歷 ReferenceQueue 中已被 GC 回收的 Key 對應 Entry,從鏈表移除並將 value=null 釋放 Value。
所有公開操作(get/put/remove/size/clear/iterator/entrySet/keySet/values)開頭均調用 expungeStaleEntries()。
List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
WeakHashMap<byte[][], byte[][]> m = new WeakHashMap<>();
m.put(new byte[1000][1000], new byte[1000][1000]); // 1MB Key + 1MB Value
maps.add(m);
System.gc(); // Key 被回收,Entry 進入 queue,但未調用 expungeStaleEntries()
}
// 結果:OOM!Value 強引用未釋放,記憶體佔用持續增長
for (int i = 0; i < 1000; i++) {
WeakHashMap<byte[][], byte[][]> m = new WeakHashMap<>();
m.put(new byte[1000][1000], new byte[1000][1000]);
maps.add(m);
System.gc();
for (int j = 0; j <= i; j++) {
maps.get(j).size(); // 觸發清理,Value 得以釋放
}
}
// 結果:記憶體穩定,無 OOM
WeakReference 參考 → 回收 Key 物件。WeakReference 入隊 ReferenceQueue。expungeStaleEntries() 輪詢隊列。entry.value = null → Value 變為不可達 → 下次 GC 回收 Value。結論:Key 回收是被動發生(GC),Value 釋放是主動觸發(訪問 Map)。若長期不訪問 Map,Value 永不釋放。
Class、Thread、String.intern() 等「不可重建」物件。Caffeine、Guava Cache 並設定 expireAfterAccess。map.put(key, new WeakReference<>(value))。Collections.synchronizedMap(new WeakHashMap<>()) 或改用 ConcurrentHashMap + WeakReference 自行管理。Cache<Key, Value> cache = Caffeine.newBuilder()
.weakKeys() // Key 弱引用
.weakValues() // Value 弱引用(可選)
.expireAfterAccess(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
Caffeine 提供精確的過期策略、統計指標、非阻塞高併發,完全替代手工 WeakHashMap 快取。
WeakHashMap Key 弱引用,Value 強引用;清理需主動訪問觸發。