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