引用数组索引会导致内存泄漏吗?

时间:2018-03-05 18:12:59

标签: java memory memory-management memory-leaks garbage-collection

我正在阅读"第6项:消除过时的对象引用" Effective Java第二版。

以下是代码段。

//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中显示为内存泄漏。

4 个答案:

答案 0 :(得分:4)

你得到了大部分内容。

如果你这样做:

elements[0] = someOtherObject;

然后不再引用存储在索引0处的其他元素,并且可能会收集它们。

但是第一个pop()实现保持该引用 - 它只会减少存储元素的“计数器”。因此,该对象仍然被引用 - 并且不会被收集直到将新对象添加到堆栈中!

由于pop()第二版中的注释明确指出 - 引用必须消除以确保堆栈不会保留对该对象的引用。应该弹出该对象 - 因此堆栈不应该保留有关删除对象的知识!

并确认提交:是的,当一个人推送 n 对象,然后推送 n 其他对象,然后你没有内存泄漏 - 因为底层数组引用将全部更新并指向新对象。是的,如果在弹出后推送少于 n 的对象,则保留过时的引用并阻止垃圾收集。

答案 1 :(得分:1)

引用Effective Java(强调我的)

  

如果堆栈增长然后收缩,则从堆栈弹出的对象将不会被垃圾收集,即使使用堆栈的程序没有更多的引用也是如此。这是因为堆栈维护对这些对象的过时引用。过时的引用只是一个永远不会再被解除引用的引用。在这种情况下,元素数组的“活动部分”之外的任何引用都是过时的。 活动部分由索引小于大小的元素组成。

他引用了弹出元素的引用。

但是,在你的例子中,你是正确的,当你在索引0存储对 new Object 的引用时,没有对第一个Object的引用,因此它有资格创建Garbage。 / p>

但是说,

  1. 您创建了五个对象(elements[0]... elements[4]
  2. 你弹出三个元素。这会使您的top变量(size此处)指向索引2.
  3. 但是,你仍然会有5个活动引用,这会阻止最后三个对象被垃圾回收。

答案 2 :(得分:1)

该问题涉及这样一个事实:数组仍然保持对仅从逻辑上弹出数组的对象的引用(减小大小计数器)。这意味着获得此内存的唯一方法是通过将整个堆栈设置为null来垃圾收集整个堆栈。

你的情况是正确的,如果你刚刚重新分配到第n个索引,它就不会是泄漏,因为你仍然期望该对象存在。但是对于pop,你的目标是减小堆栈的大小,这意味着在弹出后应该收集分配给堆栈顶部的任何内存。

答案 3 :(得分:0)

术语“内存泄漏”是从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之后下降。