系列最終篇,深度剖析 SoftHashMap 完整源碼,展示如何結合 SoftReference、ReferenceQueue、硬引用緩存雙層結構 實現「記憶體敏感的高效緩存」,徹底解決緩存導致的 OOM 風險。
設計目標
- 緩存盡可能多熱點資料提升命中率。
- JVM 記憶體不足時自動釋放軟引用物件,避免 OOM。
- 近期訪問物件保留在硬引用鏈表中(LRU 風格),延緩被回收。
完整源碼解析
public class SoftHashMap extends AbstractMap {
// 內部 HashMap 存儲 SoftReference
private final Map hash = new HashMap();
// 硬引用緩存大小上限(近期訪問物件)
private final int HARD_SIZE;
// 硬引用 FIFO 隊列(最近訪問在頭部)
private final LinkedList hardCache = new LinkedList();
// ReferenceQueue:GC 回收軟引用後通知
private ReferenceQueue queue = new ReferenceQueue();
public SoftHashMap() { this(100); }
public SoftHashMap(int hardSize) { HARD_SIZE = hardSize; }
// 核心:獲取值
public Object get(Object key) {
Object result = null;
SoftReference softRef = (SoftReference) hash.get(key);
if (softRef != null) {
result = softRef.get(); // 可能為 null(已被 GC 回收)
if (result == null) {
hash.remove(key); // 清理無效條目
} else {
// 訪問命中:加入硬引用隊列頭部(LRU)
hardCache.addFirst(result);
if (hardCache.size() > HARD_SIZE) {
hardCache.removeLast(); // 淘汰最久未訪問
}
}
}
return result;
}
// 自定義 SoftReference:攜帶 key 以便從 queue 清理 hash
private static class SoftValue extends SoftReference {
private final Object key;
SoftValue(Object value, Object key, ReferenceQueue q) {
super(value, q);
this.key = key;
}
}
// 處理 ReferenceQueue:清理已被 GC 回收的條目
public void processQueue() {
SoftValue sv;
while ((sv = (SoftValue) queue.poll()) != null) {
hash.remove(sv.key); // 關鍵:用攜帶的 key 從 hash 移除
}
}
// 放入新值
public Object put(Object key, Object value) {
processQueue(); // 先清理失效條目
return hash.put(key, new SoftValue(value, key, queue));
}
public Object remove(Object key) {
processQueue();
return hash.remove(key);
}
public void clear() {
hardCache.clear();
processQueue();
hash.clear();
}
public int size() {
processQueue();
return hash.size();
}
// 不支援 entrySet(避免暴露內部結構)
public Set entrySet() {
throw new UnsupportedOperationException();
}
}
核心機制圖解
SoftHashMap 結構:
┌─────────────────────────────────────┐
│ hash: HashMap │
│ Key → SoftValue(value, key, queue)│
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ hardCache: LinkedList │ (硬引用,防止近期熱點被 GC)
│ Head(最近訪問) ... Tail(最久未訪問) │
│ 大小限制 HARD_SIZE (預設 100) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ queue: ReferenceQueue │
│ GC 回收 SoftValue 後 enqueue │
│ processQueue() 輪詢清理 hash │
└─────────────────────────────────────┘
關鍵設計細節
| 機制 | 作用 | 關鍵代碼 |
| SoftReference | 記憶體不足時 GC 可回收值 | new SoftValue(value, key, queue) |
| ReferenceQueue | 異步通知哪些軟引用被回收 | queue.poll() 在 get/put/size/clear 時調用 |
| 硬引用緩存 | 近期訪問物件強引用持有,不被 GC | hardCache.addFirst(result) |
| LRU 淘汰 | 硬引用超限移除尾部 | hardCache.removeLast() |
| 攜帶 key | queue 中無 value 時仍能定位 hash 條目 | SoftValue.key |
| 懶性清理 | 各公共方法調用 processQueue() | get/put/remove/size/clear 首行 |
使用範例
// 緩存圖片、模板、查詢結果等「生成成本高、記憶體佔用大、可重算」資料
SoftHashMap cache = new SoftHashMap(200); // 保留 200 個硬引用
// 存入
cache.put("user:1001", loadUserFromDB(1001));
// 取出
User u = (User) cache.get("user:1001");
if (u == null) {
u = loadUserFromDB(1001);
cache.put("user:1001", u);
}
適用場景 vs 不適用場景