首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

深度解析 WeakHashMap 與 HashMap 核心差異與記憶體洩漏陷阱

     編輯:Java 相關知識

WeakHashMap 常被誤解為「自動清理無用快取的神器」,實際上其弱引用機制僅作用於 Key,且清理時機強依賴「主動訪問 Map」。本文從源碼層面剖析 WeakHashMap 與 HashMap 的本質差異,揭示「不訪問不清理」的記憶體洩漏陷阱,並給出正確使用姿勢。

一、核心結構差異

特性HashMapWeakHashMap
Key 引用型別強引用弱引用 (WeakReference<K>)
Entry 繼承體系Node<K,V> implements Map.EntryEntry<K,V> extends WeakReference<K>
Key 回收條件Map 移除或 Map 不可達Key 僅被 WeakHashMap 參考(無強引用)
Value 引用強引用強引用(同 HashMap)
清理觸發時機無自動清理調用 get/put/size/iterator 等方法時
線程安全非線程安全非線程安全

二、WeakHashMap 內部機制深度剖析

2.1 Entry 結構

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 上。

2.2 過期條目清理:expungeStaleEntries()

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。

2.3 清理觸發點

所有公開操作(get/put/remove/size/clear/iterator/entrySet/keySet/values)開頭均調用 expungeStaleEntries()。

三、經典記憶體洩漏場景重現

場景 1:建立 WeakHashMap 後從未訪問

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 強引用未釋放,記憶體佔用持續增長

場景 2:定期訪問 size() 觸發清理

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

四、Key 被回收 → Value 釋放的完整鏈路

  1. GC 掃描發現 Key 僅被 WeakReference 參考 → 回收 Key 物件。
  2. JVM 將該 WeakReference 入隊 ReferenceQueue。
  3. 下一次調用 Map 任意公開方法 → expungeStaleEntries() 輪詢隊列。
  4. 找到對應 Entry → 從哈希表移除 → entry.value = null → Value 變為不可達 → 下次 GC 回收 Value。

結論:Key 回收是被動發生(GC),Value 釋放是主動觸發(訪問 Map)。若長期不訪問 Map,Value 永不釋放。

五、正確使用姿勢與替代方案

5.1 適用場景

  • Key 為 Class、Thread、String.intern() 等「不可重建」物件。
  • 作為「非關鍵」快取,允許隨時失效(如元資料快取)。
  • 頻繁讀寫的場景(每次操作自動清理)。

5.2 避坑指南

  1. 不要用於長壽命、低頻訪問的快取 → 改用 Caffeine、Guava Cache 並設定 expireAfterAccess。
  2. Value 不得強引用 Key → 否則 Key 永不回收。需包裝:map.put(key, new WeakReference<>(value))。
  3. 多線程環境 → 包裝 Collections.synchronizedMap(new WeakHashMap<>()) 或改用 ConcurrentHashMap + WeakReference 自行管理。

5.3 生產級替代:Caffeine 快取

Cache<Key, Value> cache = Caffeine.newBuilder()
    .weakKeys()                 // Key 弱引用
    .weakValues()               // Value 弱引用(可選)
    .expireAfterAccess(10, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .build();

Caffeine 提供精確的過期策略、統計指標、非阻塞高併發,完全替代手工 WeakHashMap 快取。

六、總結

  • WeakHashMap Key 弱引用,Value 強引用;清理需主動訪問觸發。
  • 「建立後不訪問」會導致 Value 記憶體洩漏,嚴重時 OOM。
  • 快取場景優先選擇 Caffeine/Guava,僅在元資料、ClassLoader 隔離等特定場景使用 WeakHashMap。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved