Java 中的記憶體洩漏
在 Garbage 集合示例中,我們暗示 Java 解決了記憶體洩漏的問題。事實並非如此。Java 程式可能洩漏記憶體,但洩漏的原因卻相當不同。
可達物體可能會洩漏
考慮以下天真堆疊實現。
public class NaiveStack {
private Object[] stack = new Object[100];
private int top = 0;
public void push(Object obj) {
if (top >= stack.length) {
throw new StackException("stack overflow");
}
stack[top++] = obj;
}
public Object pop() {
if (top <= 0) {
throw new StackException("stack underflow");
}
return stack[--top];
}
public boolean isEmpty() {
return top == 0;
}
}
當你想要一個物體,然後立即切換它時,仍會有一個對 stack
陣列中物體的引用。
堆疊實現的邏輯意味著該引用不能返回給 API 的客戶端。如果一個物件已被彈出,那麼我們可以證明它不能 在任何可能的連續計算中從任何活動執行緒訪問 。問題是當前的 JVM 無法證明這一點。當前生成的 JVM 在確定引用是否可訪問時不考慮程式的邏輯。 (首先,這是不切實際的。)
但拋開可達性實際意義上的問題,我們顯然有一種情況,即 NaiveStack
實現掛在應該被回收的物件上。那是記憶體洩漏。
在這種情況下,解決方案很簡單:
public Object pop() {
if (top <= 0) {
throw new StackException("stack underflow");
}
Object popped = stack[--top];
stack[top] = null; // Overwrite popped reference with null.
return popped;
}
快取可能是記憶體洩漏
提高服務效能的常用策略是快取結果。我們的想法是,你將常見請求及其結果記錄在稱為快取的記憶體資料結構中。然後,每次發出請求時,都會在快取中查詢請求。如果查詢成功,則返回相應的已儲存結果。
如果實施得當,這種策略可以非常有效。但是,如果實現不正確,快取可能是記憶體洩漏。請考慮以下示例:
public class RequestHandler {
private Map<Task, Result> cache = new HashMap<>();
public Result doRequest(Task task) {
Result result = cache.get(task);
if (result == null) {
result == doRequestProcessing(task);
cache.put(task, result);
}
return result;
}
}
這段程式碼的問題在於,雖然對 doRequest
的任何呼叫都可以向快取新增新條目,但沒有任何東西可以刪除它們。如果服務不斷獲得不同的任務,則快取最終將消耗所有可用記憶體。這是一種記憶體洩漏。
解決此問題的一種方法是使用具有最大大小的快取,並在快取超過最大值時丟棄舊條目。 (拋棄最近最少使用的條目是一個很好的策略。)另一種方法是使用 WeakHashMap
構建快取,以便 JVM 可以在堆開始變得過滿時逐出快取條目。