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

问题描述 投票:3回答:4

我正在阅读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);
    }
}

根据这个项目,内存泄漏是因为在popping之后,数组索引未被引用为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中显示为内存泄漏。

java memory memory-management memory-leaks garbage-collection
4个回答
4
投票

你得到了大部分。

如果你这样做:

elements[0] = someOtherObject;

那么存储在索引0的另一个元素不再被引用并且可能被收集。

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

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

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


1
投票

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

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


1
投票

引自Effective Java(强调我的)

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

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

但是,在您的示例中,您是正确的,当您将索引存储在索引0处的新对象时,没有对第一个对象的引用,因此它有资格创建垃圾。

但是说,

  1. 你创建了五个对象(elements[0]... elements[4]
  2. 你弹出三个元素。这将使你的top变量(size在这里)指向索引2。

但是,您仍然会有5个活动引用,这将阻止最后三个对象被垃圾回收。


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之后下降,如果堆栈收缩。

© www.soinside.com 2019 - 2024. All rights reserved.