垃圾收集器(GC)雖自動管理記憶體,但若破壞物件生命週期管理,仍會發生記憶體洩漏。典型案例——LeakyChecksum 為了「優化」將緩衝區提升為實例欄位:
public class LeakyChecksum {
private byte[] byteArray; // 實例欄位緩存緩衝區
public synchronized int getFileChecksum(String fileName) {
int len = getFileSize(fileName);
if (byteArray == null || byteArray.length < len)
byteArray = new byte[len]; // 只增不減
readFileContents(fileName, byteArray);
// 計算 checksum...
}
}
後果:緩衝區永不釋放,隨最大檔案增長永久佔用記憶體,增加 GC 壓力。根本原因:邏輯上屬於方法區域的變數,被人為延長生命週期至實例層級。
弱參考不阻礙 GC 回收;軟參考則允許應用程式指定物件為「可犧牲」:記憶體充足時保留,記憶體緊張時由 GC 回收。GC 在拋出 OutOfMemoryError 前必須清除所有軟參考。
public class CachingChecksum {
private SoftReference<byte[]> bufferRef;
public synchronized int getFileChecksum(String fileName) {
int len = getFileSize(fileName);
byte[] byteArray = bufferRef.get(); // 可能返回 null
if (byteArray == null || byteArray.length < len) {
byteArray = new byte[len];
bufferRef.set(byteArray); // 更新軟參考
}
readFileContents(fileName, byteArray);
// 計算 checksum...
}
}
現在緩衝區在記憶體允許時保留,壓力大時自動回收,無需手動管理生命週期。
SoftReference<T>。SoftReference<Map<K,V>>(整個 Map 可一次回收),而非 Map<K, SoftReference<V>>(GC 工作量大)。參考物件(Reference 子類)在 GC 追蹤階段不被標記,而是放入已知活躍參考佇列。追蹤結束後:
每次 GC 必須建構活躍參考物件列表並逐一處理,帶來固定開銷。參考物件本身也受 GC 管理,可能在被參考物件前被回收(此時不入佇列)。
陣列實作堆疊/環狀緩衝區時,pop() 後陣列仍持有參考阻礙 GC:
public class LeakyStack {
private Object[] elements = new Object[MAX_ELEMENTS];
private int size = 0;
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
// elements[size] = null; // 必須手動清除!
return result;
}
}
修正:彈出後將陣列位置設為 null。鏈結實作無此問題(節點生命週期自動綁定)。若堅持陣列優化,開發者必須嚴謹管理參考生命週期。
軟參考與弱參考同樣利用 GC 協助防止物件游離。僅當應用程式可容忍大量快取物件被回收時,軟參考才為適宜選擇。