Java局部变量何时可以进行垃圾回收?

5

以下是程序代码:

import java.io.*;
import java.util.*;

public class GCTest {

    public static void main(String[] args) throws Exception {
        List cache = new ArrayList();
        while (true) {
            cache.add(new GCTest().run());
            System.out.println("done");
        }
    }

    private byte[] run() throws IOException {
        Test test = new Test();
        InputStream is = test.getInputStream();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buff = new byte[256];
        int len = 0;
        while (-1 != (len = is.read())) {
            baos.write(buff, 0, len);
        }
        return baos.toByteArray();
    }

    private class Test {
        private InputStream is;

        public InputStream getInputStream() throws FileNotFoundException {
            is = new FileInputStream("GCTest.class");
            return is;
        }

        protected void finalize() throws IOException {
            System.out.println("finalize");
            is.close();
            is = null;
        }
    }
}

你认为在run方法中的while循环仍在执行且本地变量test仍在作用域内时,finalize方法是否会被调用?
更重要的是,这种行为是否有定义?Sun有没有声明这是实现定义?
这与SO上以前提出的问题相反,人们主要关注内存泄漏。这里我们有GC积极清除我们仍然感兴趣的变量。你可能会认为,因为test仍然在“作用域”内,它不会被GC。
值得一提的是,根据JVM实现的不同,有时测试“有效”(即最终会出现OOM),有时会失败。
顺便说一句,我并不是在为这段代码辩护,这只是一个在工作中遇到的问题。

1
只是一些小问题:严格来说,“局部变量”从不符合GC的条件,因为它们只能保存引用或原始值,而只有对象才会被GC。但是,由局部变量引用的对象可能会变得符合GC的条件。 - Joachim Sauer
6个回答

15
尽管对象在作用域内时不会被垃圾回收,但如果变量在代码中不再被使用(这就是你所看到的不同行为),JIT编译器可能会将其排除在作用域之外,尽管在阅读源代码时,变量似乎仍然处于"作用域"之内。
我不明白为什么你在代码中不再引用一个对象后还关心它是否被垃圾回收,但如果你想确保对象保留在内存中,最好的方法是直接在类的字段中引用它们,甚至更好的是在静态字段中引用。如果静态字段引用了该对象,它就不会被垃圾回收。
编辑:这里是你要找的明确文档。
我假设一个对象在局部引用离开作用域之前不会死亡。
这个假设是不成立的。Java规范和JVM规范都没有保证这一点。
仅仅因为一个变量在作用域内,并不意味着它所指向的对象是可达的。通常情况下,一个在作用域内的变量所指向的对象是可达的,但你的情况是一个例外。编译器可以在即时编译时确定哪些变量是无效的,并且不会将这些变量包含在oop-map中。由于"nt"所指向的对象无法从任何活动变量访问到,它可以被回收。

我们关心的原因是因为 finalize 方法。这导致程序失败。是的,有简单的方法可以修复它,但提出此问题的人认为在 run 方法退出之前不应该将测试实例 GC 掉,因为它仍然处于“范围内”。这个测试用例是一个更大、更复杂的应用程序的简化版本。 - Dave Griffiths
7
@Dave: 那个人是错误的。简单明了地说,Java 语言规范和虚拟机规范并不保证一个“隐形”的引用不会被垃圾回收。 - Stephen C
优秀的链接Yishai,谢谢!我发现这个案例更令人惊讶——即使它作为参数传递给另一个方法,它仍然符合GC的条件。 - Dave Griffiths
5
请记住,Hotspot虚拟机比StackOverflow上的每个人都聪明得多。 - skaffman

8
我建议你和同事阅读《垃圾收集的真相》。在开始时,它说:
规范中对Java平台的垃圾收集方式几乎没有做出任何承诺。虽然这可能看起来令人困惑,但事实上,这种垃圾收集模型不是被严格定义的,这一点非常重要和有用。如果垃圾收集模型被严格定义,可能会在所有平台上无法实现,类似地,它可能排除了有用的优化,并长期损害平台的性能。
在您的示例中,变量test在while循环中变得“不可见”(见上文A.3.3)。此时,一些JVM将继续将变量视为包含“硬引用”,而另一些JVM将其视为变量已被置空。任何一种行为都可以符合兼容的JVM。
引用JLS版本3(第12.6.1节第2段)的话:
可达对象是指可以从任何活动线程的潜在持续计算中访问的任何对象。
请注意,可达性根本没有按照作用域进行定义。引用的文本继续如下:
可以设计优化程序的变换,将被认为是可达的对象数量减少到低于那些天真地被认为是可达的对象。例如,编译器或代码生成器可以选择将不再使用的变量或参数设置为空以使得此类对象的存储更早地可回收。
(我的强调)。这意味着对象可能会在你期望之前或之后进行垃圾回收和终结。还值得注意的是,一些JVM需要多个GC周期才能完成不可达对象的终结。
最重要的是,依赖终结在早期或晚期发生的程序本质上是不可移植的,并且在我看来存在缺陷。

我认为这句话可能有点误导性,因为它仍然鼓励我们以堆栈帧中的本地变量槽位为思考方式,而JIT编译器可能会优化掉任何这样的概念。早期的引用可能更准确。例如,另一位同事建议在while循环之后放置“test = null;”可以防止GC,因为它使变量“正在使用”,但实际上没有任何区别(不计算为“计算”)。 - Dave Griffiths
我建议你告诉你的同事,试图猜测JIT编译器可能会做什么是一个坏主意。此外,你需要记住JLS并不打算让普通人容易理解。主要要求是精确性,而不是可读性。要真正理解它所说的(和未说的),你需要仔细地、追求严谨地阅读它。 - Stephen C
当然,为了编写正确的代码,您需要轻松理解规则。您引用的链接提到不可见性是开发人员困惑的源头,但随后又通过引用try/catch块的范围来增加了困惑。您引用的可达定义在我看来更易于理解。 (当然,如果开发人员没有以他们现在的方式使用finalizers,那么这些都不会成为问题!) - Dave Griffiths
1
当然,如果开发人员不像他们现在这样使用 finalisers,那么这就不会是一个问题!事实上,你/他们根本不应该使用 finalizers。我从来没有遇到过它们是一个好解决方案的情况。 - Stephen C

4

有点偏题,但是 finalize() 不应该被用来关闭文件。语言不保证 finalize() 会被调用。始终使用 try ... finally 结构来确保文件关闭、数据库清理等。


1
你觉得奇怪的是什么?每次执行run()时,都会创建Test的新实例。一旦运行完成,该test实例就超出范围并有资格进行垃圾回收。当然,“有资格进行垃圾回收”和“已被垃圾回收”并不是同一回事。我期望如果你运行这个程序,你会看到一堆finalize消息随着run调用的完成而滚动。由于我所看到的唯一控制台输出是这些消息,我不知道你在每条消息中看到哪个Test实例正在被清理。如果你在每次调用run之前添加println语句,甚至在Test对象中添加一个计数器,每次创建新对象时将其递增,并将其与finalize消息一起输出,那么你就可以看到真正发生的事情了。(好吧,也许你正在使用调试器运行这个程序,但这可能也会更加模糊。)

不,测试实例正在 while 循环期间和 run 完成之前被 GC 回收。因为它不再被积极使用(尽管在通常理解的“作用域”内)。试一下就知道了。 - Dave Griffiths
我并不怀疑编译器可以进行足够的优化来解决这个问题。我的观点是:你怎么知道呢?我在那个程序的输出中没有看到任何能告诉你这一点的东西。 - Jay

0

由于test仅被使用一次,因此在调用后可以立即删除它。即使每次对read的调用都使用对getInputStream的调用而不是使用本地的is变量,也可以优化对象的使用。由于FIleInputStream使用锁定,因此无法过早地完成最终操作。最终器很难处理。

无论如何,您的最终器都是无意义的。基础的FileInputStream将在最终操作时自行关闭。


我知道,但这并不是我的代码,它只是一个简化版的测试用例,其依赖于“范围内”的变量不会消失的较大应用程序。我最感兴趣的是是否有任何定义范围概念并说明实现何时可以自由垃圾回收的文档。因为许多人认为上面的测试应该有效,因为变量只有在运行方法返回时才会超出范围(并变得符合垃圾回收要求)。 - Dave Griffiths
它被定义为Java内存模型的一部分(在JLS第3版和JVM规范第2版的更新中)。 - Tom Hawtin - tackline

0
理论上,测试不应该在范围内,因为它是在方法级别的run()中运行的,当你退出方法时,局部变量应该被垃圾回收。然而,你正在将结果存储在列表中,我曾经读到过某个地方说,列表容易存储弱引用,这些引用不容易被垃圾回收(取决于JVM实现)。

@Rajat:你的理论只有在JVM可以在列表中存储完全不相关的引用时才是可行的。这将是对规范如此严重的违反,以至于我会排除任何生产质量JVM的可能性。 - Stephen C
你可能是对的,Stephen。我认为我需要进一步阅读和研究这个问题。 - Rajat

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