鉴于以下计划:
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是否有任何声明它是实现定义的?
这与之前询问此问题的方式相反,人们主要关注的是内存泄漏问题。在这里,我们让GC积极地GCing我们仍然感兴趣的变量。你可能会期望,因为测试仍在“范围内”,它不会是GC。
对于记录,似乎有时测试“有效”(即最终命中OOM),有时它会失败,具体取决于JVM的实现。
不要捍卫这段代码的编写方式BTW,这只是一个工作中出现的问题。
答案 0 :(得分:14)
虽然如果对象仍在范围内,它将不会被垃圾收集,但如果该代码中没有实际使用该变量,则JIT编译器可能会将其从范围中取出(因此您看到的行为不同)即使你读取源代码,变量似乎仍然“在范围内”。
如果您不再在代码中引用对象,我不明白为什么您关心对象是垃圾收集,但是如果您想确保对象留在内存中,最好的方法是直接在字段中引用它们一个类,甚至更好的静态字段。如果静态字段引用该对象,则不会收集垃圾。
编辑:Here是您要查找的明确文档。
>
我假设一个对象在之前无法死亡对它的本地引用已超出范围。这不能假设。不是 Java规范和JVM规范保证 此
仅仅因为变量在范围内, 并不意味着它指向的对象 可以到达。通常是这种情况 一个物体指向的物体 范围内变量是可达的,但是 你的情况并非如此。该 编译器可以在jit时确定 哪些变量是死的而不是 在oop-map中包含这些变量。 由于“nt”指向的对象 可以[原文如此 - 应该不能] 从任何实时变量到达,它是 有资格收藏。
答案 1 :(得分:8)
我建议您和您的同事阅读The Truth About Garbage Collection。
一开始,它说:
Java的规范 平台很少有人承诺 垃圾收集实际上如何工作。 [消隐]
虽然看起来令人困惑,但事实 垃圾收集模型是 实际上并没有严格定义 重要且有用的 - 严格定义的 垃圾收集模型可能是 不可能全部实施 平台。同样,它可能 排除有用的优化和伤害 平台的性能 长期。
在您的示例中,test
循环中while
变量变为“不可见”(参见上面的A.3.3)。此时,一些JVM将继续将变量视为包含“硬引用”,而其他JVM将视为变量已被置零。这两种行为都适用于兼容的JVM
引自JLS第3版(第12.6.1节第2段):
可达对象是任何对象 可以在任何潜力中访问 从任何现场继续计算 线。
请注意,可访问性根本没有根据范围定义。引用文本如下:
优化转换 可以设计一个减少的程序 对象的数量 可达到小于那些 天真地被认为是可达的。 例如,编译器或代码 生成器可以选择设置变量 或者不再是的参数 用于null以导致存储 这样的对象是潜在的
(我的重点补充说。)这意味着对象对象可能被垃圾收集,并且可能比您预期的更早或更晚地进行终结。值得注意的是,在完成无法访问的对象之前,某些JVM需要多个GC周期。
最重要的是,依赖于早期或晚期发生的终结的程序本质上是不可移植的,并且在我看来是错误的。
答案 2 :(得分:3)
稍微偏离主题,但finalize()永远不应该用于关闭()文件。该语言不保证finalize()将被调用。始终使用try ... finally结构来保证文件关闭,数据库清理等等。
答案 3 :(得分:1)
你有什么观察到你觉得奇怪?每次执行run()时,都会创建一个新的Test实例。运行完成后,该测试实例超出范围并符合垃圾回收条件。当然“符合垃圾收集条件”和“垃圾收集”并不是一回事。我希望如果你运行这个程序,你会看到一堆终结消息作为run complete的调用滚动。作为我看到的唯一控制台输出是这些消息,我看不出当你看到每条消息时你怎么知道哪个Test实例正在最终确定。如果在每次运行调用开始时添加了println,可能会获得更有趣的结果,甚至可能会为Test对象添加一个计数器,每次创建一个新对象时都会递增,并使用finalize消息输出。然后你可以看到真正发生的事情。 (好吧,也许你用调试器来运行它,但这也可能会模糊不清。)
答案 4 :(得分:0)
由于test
仅使用一次,因此可以在调用后立即将其删除。即使每次调用read
都使用getInputStream
调用而不是使用本地is
变量,也可以优化对象的使用。由于使用了锁定,FIleInputStream
无法提前完成。终结者很难。
无论如何,你的终结者毫无意义。无论如何,基础FileInputStream
将在最终确定时关闭。
答案 5 :(得分:0)
理论上,测试不能在范围内,因为它在方法级别run(),并且当你从方法中出来时应该对局部变量进行垃圾收集。但是你将结果存储在列表中,我有读取它的某些列表很容易存储不易被垃圾收集的弱引用(取决于jvm实现)。