【问题标题】:Does referencing array index creates a memory leak?引用数组索引会造成内存泄漏吗?
【发布时间】:2018-08-13 11:45:00
【问题描述】:

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

下面是代码sn-p。

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

【问题讨论】:

  • 堆栈示例不同,因为它的想法是从无法再访问的 未使用 索引中删除对象引用。如果你只是将一个对象重新分配给一个仍然可以访问的索引,GC​​ 最终会处理前一个对象。

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


【解决方案1】:

术语“内存泄漏”是从 C 中借用的,在 Java 中经常被误用。 C 意义上的内存泄漏是在堆上分配的一系列字节,在代码中没有引用,因此无法释放。例如:

// ...
char* leak = malloc(10); // Local reference to heap
return;                  // reference lost

在 Java 中这种泄漏是不可能的,因为任何丢失的引用都会受到 GC 的影响。 但是,在某些情况下,可能会导致 Java 代码使用的内存超出应有的范围。您的代码代表了此类行为的许多可能示例之一。在您的情况下,正如在之前的答案中所解释的那样,堆栈的某些元素将保留在堆中,只是因为数组持有对不再需要的对象的引用。在 GC 环境中,这通常称为“延迟对象”。在 Java 中发现内存使用问题的一个好方法是在 GC 之后检查使用中的堆。如果 GC 后的堆使用率持续上升,则可能存在延迟对象或其他内存分配问题。例如,如果使用的堆在第 1 次 GC 后为 1M,在第 2 次 GC 后为 2M,在第 3 次 GC 后为 3M - 您可能应该使用 Java Memory Profiler 来查明问题。请注意,在您的示例中,GC 之间的堆使用量不会上升,但也不会下降。如果你为未使用的对象分配 null,如果堆栈收缩,堆使用将在 GC 之后下降。

【讨论】:

    【解决方案2】:

    你得到了大部分。

    如果你这样做:

    elements[0] = someOtherObject;
    

    那么存储在索引 0 处的 other 元素不再被引用,可能会被收集。

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

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

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

    【讨论】:

    • 感谢您抽出宝贵时间回答。好的,我知道了。您能否确认我对以下 2 项的理解 - 我将 5 个元素推送到该数组(现在数组大小为 5),然后我确实弹出了 5 次(现在数组大小为 0),现在我再次推送了 5 个元素,现在最后我没有内存泄漏,因为所有 5 个索引都重新引用到其他对象,对吗?只有假设我推送 10K 个元素,弹出 9K(可能一个一个或全部一起)然后再次推送 1K 个元素,那么内存泄漏才会出现,那么会有 8K 对象的内存泄漏,对吧?
    • @pjj 欢迎您。你是对的 - 查看我的答案的更新。
    【解决方案3】:

    引用 Effective Java(强调我的)

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

    他指的是弹出的元素的引用。

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

    但是说,

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

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

    【讨论】:

    • 好的。感谢您抽出宝贵时间回答。
    【解决方案4】:

    问题在于数组仍然持有对仅从逻辑上弹出数组的对象的引用(减小大小计数器)。这意味着要取回此内存的唯一方法是将整个堆栈设置为 null 来进行垃圾收集。

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

    【讨论】:

    • 好的。感谢您抽出宝贵时间回答。
    猜你喜欢
    • 2019-03-21
    • 1970-01-01
    • 2020-04-17
    • 2022-10-12
    • 2012-04-09
    • 1970-01-01
    • 2014-05-13
    • 2013-09-03
    • 1970-01-01
    相关资源
    最近更新 更多