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 可以在堆开始变得过满时逐出缓存条目。