此 Java 示例是否会导致内存泄漏?

2022-09-02 20:26:10

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

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

Used Memory计算公式为:runTime.totalMemory() - runTime.freeMemory()

问题:如果存在,是否存在内存泄漏? 是无法访问的,那么为什么它不能被释放呢?l = null;l


答案 1

上面的代码中没有内存泄漏。

一旦您将代码块括在 中,该变量就会超出范围,并且它是垃圾回收的候选项,无论您是否将其设置为 first。{}lListnull

但是,在代码块之后,直到返回该方法,都处于称为不可见的状态。虽然这是真的,但JVM不太可能自动清空引用并收集 的内存。因此,显式设置可以帮助 JVM 在进行内存计算之前收集内存。否则,当方法返回时,它将自动发生。ListListl = null

对于不同的代码运行,您可能会得到不同的结果,因为您永远不知道垃圾回收器何时运行。你可以建议你认为它应该运行 使用(即使没有设置,它甚至可能收集不可见的),但没有承诺。它在 System.gc() 的 javadoc 中声明:System.gc()Listl = null

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


答案 2

我认为这里有一些语义问题。“内存泄漏”通常意味着由程序(软件等)将一些数据存储在内存中,并使该程序进入无法再访问该内存中数据以清理它的状态,从而进入无法声明该内存以供将来使用的情况。据我所知,这是一般定义。

术语“内存泄漏”的实际用法通常是指编程语言,开发人员可以手动为他打算放在堆上的数据分配内存。这些语言是C,C++,Objective-C(*)等。例如,“malloc”命令或“new”运算符都为将放置在堆内存空间中的类的实例分配内存。在这样的语言中,如果我们以后想要清理它们使用的内存(当不再需要它们时),则需要保留一个指针来指向那些这样分配的实例。继续上面的示例,引用使用“new”在堆上创建的实例的指针稍后可以通过使用“delete”命令并将其指针作为参数传递到内存中“删除”。

因此,对于此类语言,内存泄漏通常意味着将数据放在堆上,随后要么:

  • 进入不再有指向该数据的指针的状态,或者
  • 忘记/忽略手动“取消分配”堆上数据(通过其指针)

现在,在“内存泄漏”的这种定义的背景下,Java几乎从未发生过这种情况。从技术上讲,在Java中,垃圾回收器的任务是决定何时不再引用堆分配的实例或超出范围并清理它们。Java中没有C++“删除”命令的等价物,甚至允许开发人员从堆中手动“取消分配”实例/数据。即使将实例的所有指针设为 null 也不会立即释放该实例的内存,相反,它只会使其“可垃圾回收”,将其留给垃圾回收器线程在进行扫描时进行清理。

现在,Java中可能发生的另一件事是永远不要放弃指向某些实例的指针,即使它们在给定点之后将不再需要。或者,为某些实例提供一个对于它们所使用的范围来说太大了。这样,它们将在内存中停留的时间比需要的时间长(或者永远,永远意味着直到JDK进程被杀死),因此即使从功能的角度来看,它们也不会被垃圾收集器收集。这可能导致类似于更广泛意义上的“内存泄漏”的行为,其中“内存泄漏”仅代表“在不再需要时在内存中拥有内容并且无法清理它”。

现在,正如你所看到的,“内存泄漏”有点模糊,但从我所看到的,你的示例不包含内存泄漏(即使是你不做l=null的版本)。所有变量都在一个由荣誉块分隔的紧密范围内,它们在该块内使用,并且在块结束时将超出范围,因此它们将被“正确”地“垃圾回收(从程序的功能角度来看)。正如@Keppil所述:将指针设置为 null 将为 GC 提供更好的提示,说明何时清理其相应的实例,但即使您从未将其设置为 null,您的代码也不会(不必要地)挂在实例上,因此不会在那里发生内存泄漏。

Java 内存泄漏的一个典型示例是将代码部署到 Java EE 应用程序服务器中时,这样它将生成超出所述应用程序服务器控制范围的线程(映像启动 Quartz 作业的 servlet)。如果多次部署和取消部署应用程序,则某些线程可能不会在取消部署时被终止,但在部署时(重新)启动,从而使它们和它们可能创建的任何实例在内存中无用地挂起。

(*)更高版本的Objective-C还提供了自动管理堆内存的可能性,其方式类似于Javas垃圾回收机制。


推荐