为什么这段代码会导致内存泄漏?

3
在大学里,我们被提供了以下代码示例,并被告知运行此代码时存在内存泄漏。该示例应该证明这是垃圾收集器无法工作的情况。
就我的面向对象编程而言,唯一能够创建内存泄漏的代码行是:
items=Arrays.copyOf(items,2 * size+1); 
文档称,元素被复制。这是否意味着引用被复制(因此在堆上创建了另一个条目),还是对象本身被复制?据我所知,Object和因此Object[]都实现为引用类型。因此,将新值分配给“items”将允许垃圾收集器发现旧的“item”不再被引用,因此可以被回收。
在我看来,这个代码示例并不会产生内存泄漏。有人能证明我错了吗?=)
import java.util.Arrays;
public class Foo  
{  
private Object[] items;  
private int size=0;  
private static final int ISIZE=10;

public Foo()  
{  
  items= new Object[ISIZE];  
}  

public void push(final Object o){  
  checkSize();  
  items[size++]=o;  
}  

public Object pop(){  
  if (size==0)  
    throw new ///...  
  return items[--size];  
}  
private void checkSize(){  
  if (items.length==size){  
    items=Arrays.copyOf(items,2 * size+1);  
  }  
}  
}

1
你提出的问题是合理的:这段代码是否实际上存在内存泄漏? - msw
1
只要 Foo 实例没有被垃圾回收,你就浪费了内存。如果 Foo 实例的生命周期很长,它就接近于“真正”的内存泄漏。但无论是真实的泄漏还是不是,代码都很糟糕,并且浪费了不必要的内存。 - Petar Minchev
@Petar:糟糕的代码啊,你必须经过一些奇怪的扭曲来使引用无法到达。这个例子是如此人为,以至于似乎没有什么教育意义。 - msw
这是一个新的Stack实现吗?代码类似于我的教授用于重新实现数据结构的示例。 - Jason
10个回答

11

pop()方法会导致内存泄漏。

原因在于你只是减少了队列中的项目数量,但实际上并没有将它们从队列中删除。引用仍然存在于数组中。如果你不删除它们,即使执行生成对象的代码,垃圾回收器也不会销毁这些对象。

想象一下:

{
    Object o = new Object();
    myQueue.add(o);
}

现在你只有一个指向这个对象的引用 - 在数组中。

后来你做了:

{
    myQueue.pop();
}

这个弹出窗口并不会删除引用。如果您不删除引用,垃圾收集器将认为您仍在考虑使用此引用,并且该对象是有用的。

因此,如果您用 n 个对象填充队列,那么您将保留这些 n 个对象的引用。

这就是你老师告诉你的内存泄漏


如果堆栈中的元素被覆盖,它可以被垃圾回收,但在某些情况下,它可能会有泄漏,但从理论上讲,它并没有。 - Pindatjuh
1
即使被覆盖的对象比覆盖它的对象更容易泄漏内存 :) 不要忘记这一点。 - Leni Kirilov

9
提示:泄漏在pop方法中。考虑弹出对象的引用会发生什么...

这取决于copyOf方法的确切作用。如果它只复制引用,那就没有问题。 如果它真的“复制”了项目,那么由pop方法返回的对象仍然引用一个可能不在copyOf执行后的新数组中的对象。我看得对吗? - citronas
@citronas - Array.copyOf 只会将源数组中的引用复制到目标数组中,而不会创建源数组中对象的副本。 - Stephen C
此外,pop() 不会调用 copyOf,因此它的行为甚至与我的“提示”没有直接关系。 - Stephen C

6

这里不是先验真实存在内存泄漏。

我认为教授的意思是你没有将弹出的项置空(换句话说,在返回items[--size]之后,你可能应该设置items[size] = null)。但是当Foo实例超出范围时,所有内容都会被收集。所以这是一个相当微弱的练习。


将items[size]设置为null会导致销毁返回的对象。如果我弹出一个项目,然后将其设置为null,那么我确实会得到刚弹出的项目为null的结果。我哪里做错了? - citronas
6
他的意思大概是这样的:"Object result = items[size - 1]; items[size - 1] = null; size--; return result;"。 "result" 变量仍然指向该对象。 - Petar Minchev

4

这段代码示例并不会造成内存泄漏。当你调用pop()时,适当对象的内存确实没有被释放 - 但是在下一次调用push()时,它将被释放。

虽然这个示例从未释放内存,但是未释放的内存总是被重新使用。在这种情况下,它并不完全符合内存泄漏的定义。

for(int i = 0; i < 1000; i++)
    foo.push(new Object());
for(int i = 0; i < 1000; i++)
    foo.pop();

这将产生未被释放的内存。然而,如果您再次运行循环,或者运行一百万亿次,您也不会产生更多未被释放的内存。因此,内存永远不会泄漏。

实际上,您可以在许多malloc和free(C)实现中看到这种行为-当您释放内存时,它实际上并没有返回给操作系统,而是添加到列表中以便下次调用malloc时返回。但我们仍然不建议使用free泄漏内存。


4

这个例子在Effective Java(Java高效编程)一书中被Joshua Bloch讨论过。泄漏发生在弹出元素时,引用仍指向不再使用的对象。


2
这个答案应该有剧透警告!! - Stephen C

4

内存泄漏是由持续执行引起的分配不受限制的增长。

提供的解释说明了对象如何在弹出后通过堆栈中的引用继续保持活动状态,并且可能导致各种各样的错误行为(例如,当调用者释放他们认为是最后一个引用并期望进行最终处理和内存恢复时),但几乎不能称之为泄漏。

随着堆栈用于存储其他对象引用,以前的孤立对象将变得真正无法访问,并返回到内存池。

您最初的怀疑是正确的。所呈现的代码将提供有界的内存使用增长,并收敛到长期状态。


1

考虑这个演示:

Foo f = new Foo();
{
    Object o1 = new Object();
    Object o2 = new Object();
    f.push(o1);
    f.push(o2);
}

f.pop();
f.pop();

// #1. o1 and o2 still are refered in f.items, thus not deleted

f = null;

// #2. o1 and o2 will be deleted now

Foo中应该改进几件事情,这将解决这个问题:

  1. pop中,您应该将items条目设置为null
  2. 您应该引入与checkSize相反的东西,类似于shrinkSize,它将使数组变小(可能以类似于checkSize的方式)。

1
提示:想象一下,如果您使用一个Foo对象,向其中插入10000个“重”项目,然后使用pop()将它们全部删除,因为您在程序中不再需要它们,会发生什么。

1

我不会直接给你答案,但看看push(Object o)做了什么,而pop()没有做。


1
在pop()方法中,尺寸上的项(即items[size-1])未设置为NULL。因此,虽然大小已经减少了一个,但仍存在从对象items到items[size-1]的引用。在GC期间,即使没有其他对象指向它,items[size-1]也不会被收集,这会导致内存泄漏。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接