Java 擁有自動垃圾回收(GC)機制,但這並不意味著記憶體洩漏不會發生。本文將深入剖析 JVM 中對象的三種可達性狀態、GC 判定邏輯、finalize 方法的角色,並通過一個經典的「棧實現」案例演示如何因疏忽導致記憶體洩漏,最後給出修正方案與最佳實踐。
對於 GC 執行緒而言,堆上對象僅有三種狀態:
finalize() 尚未執行;在 finalize() 中若重新建立引用,可轉回可觸及。finalize() 已執行(或無此方法)且無引用,只有此時 GC 才會回收記憶體。finalize()、何時釋放記憶體。System.gc() / Runtime.gc() 僅是建議,不保證立即執行。因程式設計缺陷,導致本應廢棄的對象持續被引用,GC 無法回收,造成記憶體佔用持續增長,最終可能拋出 OutOfMemoryError。
public class MyStack {
private Object[] elements;
private int size = 0;
public void push(Object o) {
ensureCapacity();
elements[size++] = o;
}
public Object pop() {
if (size == 0) throw new EmptyStackException();
return elements[--size]; // 錯誤:未清除陣列中殘留的引用
}
// ... 省略擴容邏輯
}
後果:彈出後,elements[size] 仍持有對象引用,GC 認為對象「可觸及」,記憶體永不釋放。
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 關鍵:手動切斷引用
return result;
}
| 場景 | 原因 | 對策 |
|---|---|---|
| 集合類未清理 | List/Map 持有過期對象 | 及時 remove()、clear() 或置 null |
| 靜態集合 | 生命週期等同類,易累積 | 避免靜態持有大量數據,或用弱引用 |
| 監聽器/回調未移除 | 註冊後未反註冊 | 在 destroy/onStop 中移除 |
| ThreadLocal 洩漏 | 線程池複用導致值不釋放 | 用完即 remove() |
| 內部類持有外部引用 | 非靜態內部類隱含外部引用 | 改靜態內部類或弱引用 |
.hprof,定位支配樹、GC Roots 路徑。try-with-resources、AutoCloseable、WeakReference、SoftReference 善用。GC 是安全網,不是免死金牌。理解可達性分析、finalize 機制、常見洩漏模式,養成「用完即清引用」習慣,才是寫出穩健 Java 程式的關鍵。