隐形引用在最近的JVM中仍然是一个问题吗?

我正在阅读Java平台性能 (遗憾的是,因为我最初提出这个问题,链接似乎已经从互联网上消失了),而A.3.3节让我很担心。

我一直在假设退出范围的变量不再被视为GC根,但本文似乎与此相矛盾。

最近的JVM,特别是Sun的1.6.0_07版本,仍然有这个限制吗? 如果是这样,那么我有很多代码要分析……

我问这个问题是因为这篇论文是从1999年开始的 – 有时情况发生了变化,特别是在GC世界。


由于论文不再可用,我想解释一下这个问题。 本文暗示,在方法退出之前,在方法中定义的变量将被视为GC根,而不是直到代码块结束。 因此,必须将变量设置为null,以允许引用的Object被垃圾回收。

这意味着在main()方法中的条件块中定义的局部变量(或包含无限循环的类似方法)将导致一次性内存泄漏,除非在它退出范围之前将变量置为空。

所选答案的代码很好地说明了这个问题。 在文档中引用的JVM版本中,当foo对象在try块结束时超出范围时,不能对其进行垃圾回收。 相反,JVM将保持打开引用直到main()方法结束,即使任何东西都不可能使用该引用。

这似乎是一个想法的起源,即使变量即将退出范围,将变量引用置零将有助于垃圾收集器输出。

这段代码应该清除它:

public class TestInvisibleObject{ public static class PrintWhenFinalized{ private String s; public PrintWhenFinalized(String s){ System.out.println("Constructing from "+s); this.s = s; } protected void finalize() throws Throwable { System.out.println("Finalizing from "+s); } } public static void main(String[] args) { try { PrintWhenFinalized foo = new PrintWhenFinalized("main"); } catch (Exception e) { // whatever } while (true) { // Provoke garbage-collection by allocating lots of memory byte[] o = new byte[1024]; } } } 

在我的机器(jdk1.6.0_05)上打印:

从主要建构

从主要结束

所以看起来问题已得到解决。

请注意,使用System.gc()而不是循环不会导致由于某种原因收集对象。

文章指出:

…当引用超出范围时,JVM的高效实现不太可能将引用归零

我认为这是因为这样的情况:

 public void doSomething() { for(int i = 0; i < 10 ; i++) { String s = new String("boo"); System.out.println(s); } } 

这里,String的每个声明中的“高效JVM”使用相同的引用,但如果GC没有启动,则堆中将有10个新的字符串。

在文章示例中,我认为对foo的引用保留在堆栈中,因为“高效JVM” 认为很可能会创建另一个foo对象,如果是这样,它将使用相同的引用。 思考???

 public void run() { try { Object foo = new Object(); foo.doSomething(); } catch (Exception e) { // whatever } while (true) { // do stuff } // loop forever } 

我还用剖析执行了下一个测试:

 public class A { public static void main(String[] args) { A a = new A(); a.test4(); } public void test1() { for(int i = 0; i < 10 ; i++) { B b = new B(); System.out.println(b.toString()); } System.out.println("b is collected"); } public void test2() { try { B b = new B(); System.out.println(b.toString()); } catch (Exception e) { } System.out.println("b is invisible"); } public void test3() { if (true) { B b = new B(); System.out.println(b.toString()); } System.out.println("b is invisible"); } public void test4() { int i = 0; while (i < 10) { B b = new B(); System.out.println(b.toString()); i++; } System.out.println("b is collected"); } public A() { } class B { public B() { } @Override public String toString() { return "I'm B."; } } } 

并得出结论:

teste1 - > b被收集

teste2 - > b是不可见的

teste3 - > b是不可见的

teste4 - > b被收集

...所以我认为,在循环中,JVM在循环结束时不会创建不可见的变量,因为它们不太可能在循环之外再次声明。

有什么想法吗??

你真的有那么多代码需要分析吗? 基本上我只能看到这对于长时间运行的方法来说是一个重要问题 – 通常只是每个线程堆栈顶部的方法。

如果它现在没有固定的话,我不会感到惊讶,但我认为它不会像你似乎害怕那么重要。

问题仍然存在。 我用Java 8测试它并且可以certificate它。

你应该注意以下事项:

  1. 强制保证垃圾收集的唯一方法是尝试以OutOfMemoryError结尾的分配,因为JVM需要在抛出之前尝试释放未使用的对象。 然而,如果请求的数量太大而不能成功,则这不成立,即超出地址空间。 尝试提高分配直到获得OOME是一个很好的策略。

  2. 第1点中描述的保证GC不保证最终确定。 未指定调用finalize()方法的时间,根本不会调用它们。 因此,将finalize()方法添加到类可能会阻止其实例被收集,因此最终确定不是分析GC行为的好选择。

  3. 在局部变量超出范围后创建另一个新的局部变量将重用其在堆栈帧中的位置。 在下面的示例中,将收集对象a,因为它在堆栈框架中的位置被局部变量b占用。 但是b一直持续到main方法结束,因为没有其他局部变量占据它的位置。

     import java.lang.ref.*; public class Test { static final ReferenceQueue RQ=new ReferenceQueue<>(); static Reference A, B; public static void main(String[] s) { { Object a=new Object(); A=new PhantomReference<>(a, RQ); } { Object b=new Object(); B=new PhantomReference<>(b, RQ); } forceGC(); checkGC(); } private static void forceGC() { try { for(int i=100000;;i+=i) { byte[] b=new byte[i]; } } catch(OutOfMemoryError err){ err.printStackTrace();} } private static void checkGC() { for(;;) { Reference r=RQ.poll(); if(r==null) break; if(r==A) System.out.println("Object a collected"); if(r==B) System.out.println("Object b collected"); } } }