許多人討論內存洩漏,對 C/C++ 是老生常談,但 Java 開發者也越來越關注。本文小結供參考。
JVM 回收算法複雜,但核心目標:無引用的物件可回收。要造成洩漏,必須做到:持有對無用物件的引用!
別以為容易:既然無用,怎麼還持有引用?既然持有,怎麼會無用?
《Effective Java》經典範例:
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]; // 錯誤:未清空 elements[size]
}
private void ensureCapacity() {
if (elements.length == size) {
Object[] oldElements = elements;
elements = new Object[2 * elements.length + 1];
System.arraycopy(oldElements, 0, elements, 0, size);
}
}
}
原理:壓入 10 個元素後全部彈出,堆棧雖空,但 elements 數組仍持有這 10 個物件引用,導致它們無法被 GC 回收——符合洩漏兩條件:無用、無法回收。
修正:pop() 中應置空:Object result = elements[--size]; elements[size] = null; return result;
若堆棧使用較少,僅浪費幾 KB,內存達 GB 級影響微乎其微,且很快被回收。但兩種情況會放大後果:
public class Bad {
public static Stack s = new Stack();
static {
s.push(new Object());
s.pop(); // 這裡發生洩漏
s.push(new Object()); // 上面物件可回收,等於自愈
}
}
註冊監聽器後未在恰當時機移除,導致被監聽物件持有監聽器引用,間接持有大量資源,隨時間累積引發 OOM。
clear() 或置空引用WeakHashMap)Java 內存洩漏本質是「邏輯上無用但物理上可達」,理解 GC 根可達性分析算法是徹底解決的關鍵。