在 Java 開發中,OutOfMemoryError 往往是讓開發者聞風喪膽的「噩夢」。它不僅難以重現,排查起來更是耗時費力。但若能主動在測試環境中構造出各類 OOM 場景,並深入理解其成因,便能在生產環境發生時迅速定位問題。本文將帶你動手實作兩種經典且極具代表性的記憶體溢出案例,並剖析其背後的核心機制。
equals() 導致的無限增長class KeylessEntry {
static class Key {
Integer id;
Key(Integer id) { this.id = id; }
@Override public int hashCode() { return id.hashCode(); }
}
public static void main(String[] args) {
Map<Key, String> m = new HashMap<>();
while (true)
for (int i = 0; i < 10000; i++)
if (!m.containsKey(i))
m.put(new Key(i), "Number:" + i);
}
}
乍看之下,這段代碼似乎只會向 Map 中放入 10,000 個鍵值對,之後 containsKey 應該總是返回 true,循環應停止增長。然而現實是:元素會無限制增長,最終撐爆堆內存。
根因在於 Key 類重寫了 hashCode() 卻沒有重寫 equals()。根據 HashMap 的查找邏輯:先比較 hash 值,再調用 equals() 確認鍵是否相同。若缺少 equals(),預設使用 Object.equals()(即比較引用地址)。每次 `new Key(i)` 都會產生新對象,引用不同,導致 containsKey 永遠返回 false,重複插入同樣邏輯鍵的不同實例,Map 體積無限膨脹。
@Override
public boolean equals(Object o) {
if (o instanceof Key) {
return ((Key) o).id.equals(this.id);
}
return false;
}
補上 equals() 後,邏輯鍵相同的對象即可被正確識別,Map 大小穩定在 10,000。這也是《Effective Java》中「覆寫 equals() 時必須覆寫 hashCode()」鐵律的反面教材。
substring() 隱性引用class Stringer {
static final int MB = 1024 * 512; // 512KB
static String createLongString(int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) sb.append('a');
sb.append(System.nanoTime());
return sb.toString();
}
public static void main(String[] args) {
List<String> substrings = new ArrayList<>();
for (int i = 0; i < 100; i++) {
String longStr = createLongString(MB);
String subStr = longStr.substring(1, 10);
substrings.add(subStr);
}
}
}
在 JDK 7u6 之前,String.substring() 採用「共享底層 char[] 數組 + offset/count」的設計。上述代碼中,雖然只保留了 9 字符的子串,但每個子串都隱性持有對 512KB 原始大字符串的引用。若以 -Xmx100m 啟動,100 個子串即會佔用約 50MB,加上其他開銷極易觸發 OOM。
更隱蔽的是:測試環境與生產環境 JDK 微版本不同,可能導致「本地跑得好好的,上線就炸」的詭異現象。JDK 7u6 之後,substring() 改為複製新數組,此問題自動消失。
new String(longStr.substring(...)) 強制複製String.intern() 或顯式構建新字符串切斷引用鏈若想在代碼審查或測試中植入更隱蔽的記憶體炸彈,可考慮以下方向:
finalize() 延遲炸彈:在 finalize() 中執行耗時/分配大對象操作,導致 Finalizer 線程阻塞,間接引發 OOM,且堆棧極難追溯。ThreadLocal 中塞入大對象,且不清理,隨著線程複用導致洩漏持續累積。記憶體溢出雖可怕,但掌握其經典模式、善用工具即可化險為夷。除上述手動構造外,強烈建議在專案中接入 Plumbr、Arthas、MAT (Memory Analyzer Tool) 等自動化診斷工具,將「人肉排查」升級為「秒級定位」。
理解這兩個案例的核心——對象生命周期管理失控與JDK 內部實現變更帶來的行為差異,將讓你在面對生產環境 OOM 時多一份從容,少幾夜失眠。