这个Java示例会导致内存泄漏吗?

18

我有一个简单的例子。这个例子从一个包含10000000个随机整数的文件f中加载一个ArrayList<Integer>

doLog("Test 2");
{
    FileInputStream fis = new FileInputStream(f);
    ObjectInputStream ois = new ObjectInputStream(fis);
    List<Integer> l = (List<Integer>) ois.readObject();
    ois.close();
    fis.close();
    doLog("Test 2.1");
    //l = null; 
    doLog("Test 2.2");
}
doLog("Test 2.3");
System.gc();
doLog("Test 2.4");

当我有 l = null 时,我得到了这个日志:

Test 2                          Used Mem = 492 KB   Total Mem = 123 MB
Test 2.1                        Used Mem = 44 MB    Total Mem = 123 MB
Test 2.2                        Used Mem = 44 MB    Total Mem = 123 MB
Test 2.3                        Used Mem = 44 MB    Total Mem = 123 MB
Test 2.4                        Used Mem = 493 KB   Total Mem = 123 MB

但是当我将它移除后,我会得到下面这个日志。

Test 2                          Used Mem = 492 KB   Total Mem = 123 MB
Test 2.1                        Used Mem = 44 MB    Total Mem = 123 MB
Test 2.2                        Used Mem = 44 MB    Total Mem = 123 MB
Test 2.3                        Used Mem = 44 MB    Total Mem = 123 MB
Test 2.4                        Used Mem = 44 MB    Total Mem = 123 MB

已使用内存的计算方式为:runTime.totalMemory() - runTime.freeMemory()

问题:如果存在l = null;,会导致内存泄漏吗?l无法访问,为什么不能被释放?


1
你没有使用 l,如果你之后可能会用到它,GC 就无法销毁它。 - Uhehesh
1
@Uhehesh 它只在给定的范围内定义,因此以后不能再使用它。 - eis
3
垃圾回收的资格并不简单。一个对象可能需要多次垃圾回收才能符合回收条件。此外,调用System.gc()后不能保证垃圾回收会完成 - 它甚至无法确保运行。 - Alexandre Dupriez
在出现“l=null;”的情况下,是否存在内存泄漏?由于无法访问l,为什么不能释放它?可达性与Java代码中看到的花括号毫无关系。实际上,在读取后立即无法访问'l'(前提是os.readObject()没有在某个地方保留引用-是的,这是可能的)。 - bestsss
4个回答

28

以上代码中没有内存泄漏。

只要你离开被 {} 包裹的代码块,变量 l 就会超出其作用域范围,而无论是否先将 List 设置为 null,它都有可能被垃圾回收。

但是,在代码块之后直到方法返回之前,List 处于一种称为不可见的状态。在此期间,虚拟机不太可能自动将引用设置为空并收集 List 的内存。因此,在进行内存计算之前显式地将 l = null 可以帮助虚拟机收集内存。否则,它将在方法返回时自动发生。

您可能会在不同运行代码时获得不同的结果,因为您永远不知道垃圾回收器何时运行。您可以使用 System.gc() 建议它运行(即使没有设置 l = null,它也可能收集不可见的 List),但不能保证。在System.gc()的 javadoc 中指出:

调用 gc 方法表明 Java 虚拟机会花费精力回收未使用的对象,以便使它们当前占用的内存可用于快速重用。当方法调用返回时,Java 虚拟机已尽最大努力从所有弃用的对象中回收空间。


1
如果存在l = null;,是否会出现内存泄漏?由于l是不可访问的,为什么不能被释放?这个说法是错误的,有23个赞。在Java中,花括号对对象的可达性没有任何影响,它们甚至不会改变字节码。由于l之后没有被读取,因此从技术上讲,它是不可访问的。然而,将其设置为null可以确保即使它被保留在寄存器中,也可以被垃圾回收,前提是JIT不会优化写入并且不会优化方法边界内的可达性。 - bestsss
@bestsss:l是一个本地变量,它只存在于花括号内部,因此它们非常影响List的可达性。 - Keppil
只在源代码级别上工作... 在字节码上完全不同,在JIT编译器中的“到达定义”甚至更加扭曲。 - bestsss
2
换句话说,当代码被编译时,花括号不起任何作用。 - bestsss
@bestsss:我并不认为我的之前的回答一定是错的,但你提出了一个重要的观点。已编辑回答以解决这个问题。 - Keppil
1
通常,在缺乏优化(即解释代码)的情况下,您应该考虑方法中的所有变量都是可达的。当代码被JIT编译时,到达定义变得非常重要,因为它们控制着CPU寄存器的分配,如果一个变量没有被读取,则可以认为它是不可达的 - 这种情况相当复杂,因为控制流可能很难进行优化。除此之外,System.gc()会释放资源,除非明确禁用(对于Sun/Oracle的JVM)。引用javadoc是可以的,但问题主要是实际问题,很明显不存在泄漏。 - bestsss

4
我认为这里有一些语义问题。 "内存泄漏"通常指程序(软件等)将某些数据存储在内存中,并使该程序处于无法访问该内存中的数据以清理它的状态,从而陷入无法将该内存用于未来使用的情况。就我所知,这是一般定义。
“内存泄漏”这个术语的一个真实世界的用途通常是指编程语言,在这些语言中,开发人员需要手动分配要放置在堆上的数据的内存。这样的语言包括C、C++、Objective-C (*)等。例如,“malloc”命令或“new”运算符都会为将放置在堆内存空间中的类的实例分配内存。在这种语言中,如果我们稍后想要清除它们使用的内存(当它们不再需要时),则需要保留对这些已分配实例的指针。继续以上示例,使用“new”创建在堆上的实例的指针可以通过使用“delete”命令并将其指针作为参数传递来稍后从内存中“删除”。
因此,对于这样的语言,内存泄漏通常意味着将数据放置在堆上,随后要么:
- 到达一个没有指向该数据的指针的状态 - 忘记/忽略手动“释放”该在堆上的数据(通过其指针)
现在,在这样定义“内存泄漏”的情况下,Java几乎永远不会发生。从技术上讲,在Java中,垃圾收集器负责决定何时不再引用或超出范围的堆分配实例并清理它们。在Java中没有C++“delete”命令的等价物,甚至不允许开发人员手动从堆中“释放”实例/数据。即使使所有实例的指针为空也不会立即释放该实例的内存,而只会使其“可垃圾回收”,留给垃圾收集器线程在进行扫描时清理它。
现在,Java中可能发生的另一件事是,在某个给定点之后,永远不要放弃对某些实例的指针,即使它们将不再需要。或者,为某些实例提供过大的范围以供使用。这样,它们将停留在内存中比所需时间长(或永远停留,其中永远意味着直到JDK进程被杀死),因此尽管从功能角度来看应该清除它们,但它们不会被垃圾收集器收集。这可能导致类似于“内存泄漏”的行为,其中“内存泄漏”仅代表“在不再需要时将东西放入内存中并无法清理它”。
现在,正如您所看到的,“内存泄漏”有些含糊不清,但据我所见,您的示例中没有包含内存泄漏(即使是您不将l=null的版本)。所有变量都位于由花括号块限定的紧密作用域内,在该块内使用并在块结束时失去作用域,因此它们将被“正确”地垃圾回收(从程序的功能角度来看)。正如@Keppil所述:使指针为空会为GC提供更好的提示,以便在何时清理其相应实例,但即使您从未将其设置为空,您的代码也不会(不必要地)挂起实例,因此不会出现内存泄漏。
Java内存泄漏的典型例子是在将代码部署到Java EE应用服务器中时,它将在控制范围之外生成线程(想象一下启动Quartz作业的servlet)。如果应用程序多次部署和卸载,则可能有一些线程不会在卸载时被杀死,但也会在部署时重新启动,从而使它们及其可能已经创建的任何实例无用地挂在内存中。
(*)Objective-C的后续版本还可以自动管理堆内存,类似于Java的垃圾回收机制。

3
在Java中,我认为内存泄漏的定义很简单:任何无意中可访问到的对象。(而"可访问性"也有一个恰当的定义。) - biziclop
3
不,我的意思是“无意中可达”。由于Java有垃圾回收机制,所有不可达的对象都可以被回收,因此并不会导致内存泄漏。当你有一些本应该不可达的对象(强引用),但它们仍然保持可达状态时,就会出现内存泄漏问题。 - biziclop
1
这很重要,因为它意味着你必须做与C语言完全相反的事情:尽可能快地失去所有对象的引用。(虽然我不是很喜欢显式地将局部变量置空,除非真的有必要或者可以使代码更易读。) - biziclop
您可以阅读这篇博客文章:http://plumbr.eu/blog/what-is-a-memory-leak。我在那里尝试解释了Java如何使用GS仍然存在内存泄漏问题。 - Nikem

2
真正的答案是,除非代码被JIT编译,否则所有局部变量在方法体内都是“可达”的。
此外,花括号在字节码中绝对没有任何作用。它们只存在于源代码级别——JVM完全不知道它们的存在。将“l”设置为null实际上会释放堆栈上的引用,因此它会被真正地垃圾回收。开心的事情。
如果您使用另一种方法而不是内联块,则一切都会顺利通过,没有任何意外。
如果代码被JIT编译并且JVM编译器已经构建了 reaching-definitions (也包括这个),那么设置l=null很可能不会产生任何影响,并且无论哪种情况下都会释放内存。

对于那些希望验证此答案的人:JVM规范的2.6和2.6.1节指出(http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.6),当*方法*完成时,局部变量将被丢弃。 - meriton
花括号对类文件产生影响:每个局部变量都存储在堆栈帧的“本地变量”部分中。花括号允许在同一方法中的后续代码中重用这些插槽。这可能会使列表更早不可达。使用javap -v检查并查看“LocalVariableTable”部分。还可以在花括号后添加其他变量,并查看它们如何与花括号内的内容共享相同的插槽。 - A.H.
@A.H.,编译器可以随意重用槽位 - 只有对象引用类型和基本类型。如果一个变量没有被使用(即没有引用),编译器可以在有或没有花括号的情况下重新使用该槽位。这是混淆反编译器的一种方式。关键是运行时不可用花括号。确实,如果编译器(而不是JVM)决定如此,槽位将被重复使用,但必须在花括号后面进行声明和赋值。另外一点是:通过使用足够的DUP指令,局部变量通常可以被跳过,但javac不会这样编译(尽管我自己使用)。 - bestsss

1
问题:如果删除l = null;(没有这行代码),这是内存泄漏吗?
不是,但如果你使用这个“模式”,它会帮助垃圾回收器更容易地释放内存。

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