許多開發者認為「Java 有 GC 就不會記憶體洩漏」,這是危險的誤解。本文從 JVM 回收機制出發,深度解析 Java 記憶體洩漏的真實定義、經典案例、自癒特性、常見誤判場景,建立正確的記憶體管理認知體系。
| 維度 | C/C++ | Java |
|---|---|---|
| 記憶體管理 | 程式設計師手動 malloc/free | GC 自動回收 |
| 洩漏定義 | 分配後未釋放 (無指標引用) | 無用物件仍被引用,GC 無法回收 |
| 後果 | 記憶體耗盡、程式崩潰 | Old Gen 膨脹、Full GC 頻繁、OOM |
同時滿足:
只要引用鏈存在,GC 就視為「存活」,哪怕業務上已廢棄。
public class Stack {
private Object[] elements = new Object[10];
private int size = 0;
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) throw new EmptyStackException();
return elements[--size]; // ❌ 元素仍在陣列中被引用!
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // ✅ 手動切斷引用
return result;
}
public class Bad {
public static Stack s = new Stack();
static {
s.push(new Object());
s.pop(); // 1 個物件洩漏
s.push(new Object()); // 新物件進入,舊物件位置被覆蓋 → 自癒!
}
}
特性:靜態變數生命週期 = JVM 生命週期,但集合容量有上限,最多洩漏 capacity 個物件,屬「可控洩漏」。
public void notTooBad() {
Stack s = new Stack();
s.push(new Object());
s.pop(); // 洩漏發生
// 方法結束,s 變數出作用域 → Stack 物件不可達 → 整個陣列可回收 → 徹底自癒
}
結論:區域性洩漏隨方法結束自動癒合,99.9999% 場景無實質危害,但屬程式碼品質缺陷。
Map cache = new HashMap<>(); // 無大小限制、無過期淘汰
cache.put(key, largeObject); // 持續增長 → OOM
對策:使用 LinkedHashMap 實作 LRU、WeakHashMap、Guava Cache、Caffeine、Redis。
button.addActionListener(e -> doSomething());
// 畫面關閉時未 removeActionListener → button 持有 listener → listener 持有外部類引用 → 記憶體洩漏
ThreadLocal tl = new ThreadLocal<>();
tl.set(connection);
// 執行緒池重用執行緒,ThreadLocal 不清理 → Connection 永不釋放
鐵律:finally { tl.remove(); }
Object obj = new Object();
obj = null; // ❌ 無意義,離開作用域自動不可達
void func(Object o) {
o = new Object(); // 修改區域參考,不影響呼叫端原物件
} // 方法結束,新物件自動可回收
class A { B b = new B(this); }
class B { A a; B(A a) { this.a = a; } }
事實:現代 GC (標記-清除/壓縮) 完美處理循環引用,**不會** 造成洩漏。可能略微增加 GC 標記階段耗時,可忽略。
heapdump、oom 診斷jcmd GC.heap_dump -Xlog:gc*:file=gc.log + GCViewer/GCEasyclear()/evict()Java 記憶體洩漏 確實存在,但本質不同於 C/C++:
核心心法:「誰持有,誰負責釋放」。靜態集合、快取、ThreadLocal、監聽器是四大高危區。建立 try-finally/try-with-resources 肌肉記憶,配合 MAT 定期體檢,Java 記憶體管理完全可控。