24

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

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

最近的 JVM,尤其是 Sun 的 1.6.0_07 版本,是否还有这个限制?如果是这样,那么我有很多代码要分析......

我问这个问题是因为这篇论文是 1999 年的——有时情况会发生变化,尤其是在 GC 领域。


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

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

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

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

4

4 回答 4

6

这段代码应该清除它:

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() 代替循环不会由于某种原因导致对象被收集。

于 2008-11-07T11:33:53.737 回答
3

问题仍然存在。我用 Java 8 对其进行了测试,并且可以证明这一点。

你应该注意以下几点:

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

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

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

    import java.lang.ref.*;
    
    public class Test {
        static final ReferenceQueue<Object> RQ=new ReferenceQueue<>();
        static Reference<Object> 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");
            }
        }
    }
    
于 2013-08-23T15:01:34.523 回答
2

文章指出:

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

我认为发生这种情况是因为这样的情况:

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

在这里,“高效 JVM”在 String 的每个声明中都使用了相同的引用,但如果 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 在循环结束时不会创建不可见的变量,因为它们不太可能在循环外再次声明。

有什么想法吗??

于 2008-11-07T12:34:19.310 回答
1

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

如果它目前未修复,我一点也不感到惊讶,但我认为它可能不像你担心的那样重要。

于 2008-11-07T09:42:49.763 回答