我正在阅读Effective Java第二版的“第6项:消除过时的对象引用”。
以下是代码段。
//Can you spot the "memory leak"?
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* Ensure space for at least one more element, roughly doubling the capacity
* each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
根据这个项目,内存泄漏是因为在pop
ping之后,数组索引未被引用为NULL,如下所示:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
我的理解是假设对于给定的数组,我已经完成了elements[0] = new Object()
然后我再次执行此操作elements[0] = new Object()
然后我的第一个对象将有资格进行垃圾收集,因为我的数组的第0个索引不再指向它。
我的理解不正确吗?如果它是正确的,那么它如何在Effective Java中显示为内存泄漏。
你得到了大部分。
如果你这样做:
elements[0] = someOtherObject;
那么存储在索引0的另一个元素不再被引用并且可能被收集。
但是第一个pop()
实现保留了该引用 - 它只减少了存储元素的“计数器”。因此,仍然引用该对象 - 并且在将新对象添加到堆栈之前不会收集该对象!
由于pop()
第二版中的注释明确指出 - 必须消除引用以确保堆栈不保留对该对象的引用。该对象应该被弹出 - 因此堆栈不应该保留有关该被删除对象的知识!
并确认提交:是的,当一个推送n个对象,然后推送n个其他对象,然后你没有内存泄漏 - 因为底层数组引用将全部更新并指向新对象。是的,如果弹出后推送的对象少于n个,则会保留过时的引用并阻止垃圾收集。
问题涉及这样一个事实,即数组仍然保持对仅从逻辑上弹出数组的对象的引用(减小大小计数器)。这意味着获得此内存的唯一方法是通过将整个堆栈设置为null来垃圾收集整个堆栈。
你的情况是正确的,如果你刚刚重新分配到第n个索引,它就不会是泄漏,因为你仍然期望该对象存在。但是对于pop,你的目标是减小堆栈的大小,这意味着在弹出后应该收集分配给堆栈顶部的任何内存。
引自Effective Java(强调我的)
如果堆栈增长然后收缩,则从堆栈弹出的对象将不会被垃圾收集,即使使用堆栈的程序没有更多的引用。这是因为堆栈维护对这些对象的过时引用。过时的引用只是一个永远不会再被解除引用的引用。在这种情况下,元素数组的“活动部分”之外的任何引用都是过时的。活动部分由索引小于大小的元素组成。
他引用了弹出元素的引用。
但是,在您的示例中,您是正确的,当您将索引存储在索引0处的新对象时,没有对第一个对象的引用,因此它有资格创建垃圾。
但是说,
elements[0]... elements[4]
)top
变量(size
在这里)指向索引2。但是,您仍然会有5个活动引用,这将阻止最后三个对象被垃圾回收。
术语“内存泄漏”是从C语言中借用的,在Java中经常被滥用。 C语义中的内存泄漏是在堆上分配的字节范围,在代码中没有引用,因此无法释放。例如:
// ...
char* leak = malloc(10); // Local reference to heap
return; // reference lost
在Java中,这样的泄漏是不可能的,因为任何丢失的引用都受GC影响。但是,有些情况会导致Java代码使用的内存超出预期。您的代码代表了此类行为的许多可能示例之一。在您的情况下,正如之前的答案中所解释的那样,堆栈的某些元素将保留在堆中,因为数组正在保存对不再需要的对象的引用。在GC环境中,这通常称为“延迟对象”。在Java中发现内存使用问题的一个好方法是在GC之后检查使用中的堆。如果GC之后的堆使用率一直在上升,则可能存在延迟对象或其他内存分配问题。例如,如果在第一次GC之后使用的堆是1M,在第二次GC之后是2M,在3D GC之后是3M - 您应该使用Java Memory Profiler来查明问题。请注意,在您的示例中,堆使用率不会在GC之间上升,但也不会下降。如果为未使用的对象分配null,则堆栈使用率将在GC之后下降,如果堆栈收缩。