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 可以在堆開始變得過滿時逐出快取條目。