当java仍然在范围内时,java能否最终确定它?

Mic*_*son 21 java garbage-collection

我一直在研究我的代码中的一个错误,这个错误似乎是由一些"丑陋的"终结器代码引起的.代码看起来大致如此

public class A {
   public B b = new B();
   @Override public void finalize() {
     b.close();
   }
}

public class B {
   public void close() { /* do clean up our resources. */ }
   public void doSomething() { /* do something that requires us not to be closed */ } 
}

void main() {
   A a = new A();
   B b = a.b;
   for(/*lots of time*/) {
     b.doSomething();
   }
}
Run Code Online (Sandbox Code Playgroud)

我认为正在发生的事情是,a在第二行之后检测到没有引用main()并获得GC并最终确定终结器线程 - 当for循环仍然发生时,使用bwhile a仍然是"在范围内".

这有可能吗?是否允许java在对象超出范围之前将其作为对象?

注意:我知道在终结器中做任何事情都很糟糕.这是我继承并打算修复的代码 - 问题是我是否正确理解了根本问题.如果这是不可能的,那么更微妙的东西必定是我的bug的根源.

Stu*_*rks 25

当Java仍然在范围内时,Java能否最终确定它?

是.

但是,我在这里很迂腐.范围是一种确定名称有效性的语言概念.对象是否可以被垃圾收集(并因此最终确定)取决于它是否可达.

ajb答案几乎得到了它(+1)引用了JLS的重要段落.但我不认为它直接适用于这种情况.JLS §12.6.1也说:

一个可达的对象是可以在任何可能继续计算从任何活动线程访问的任何对象.

现在考虑将此应用于以下代码:

class A {
    @Override protected void finalize() {
        System.out.println(this + " was finalized!");
    }

    public static void main(String[] args) {
        A a = new A();
        System.out.println("Created " + a);
        for (int i = 0; i < 1_000_000_000; i++) {
            if (i % 1_000_000 == 0)
                System.gc();
        }
        // System.out.println(a + " was still alive.");
    }
}
Run Code Online (Sandbox Code Playgroud)

在JDK 8 GA上,这将a每次都完成.如果你println在最后取消注释,a将永远不会最终确定.

通过println注释,可以看到可达性规则如何应用.当代码到达循环时,线程无法获得任何访问权限a.因此它无法访问,因此需要进行最终确定和垃圾收集.

请注意,名称a仍然在范围内,因为可以使用a封闭块中的任何位置 - 在本例中为main方法体 - 从其声明到块的末尾.JLS§6.3中涵盖了确切的范围规则.但实际上,正如您所看到的,范围与可达性或垃圾收集无关.

为了防止对象被垃圾收集,您可以在静态字段中存储对它的引用,或者如果不想这样做,您可以稍后在相同的方法中使用它来保持它可访问环.调用一个无害的方法toString就足够了.

  • 好吧,如果迂腐匹配,那就更好了.在这项业务中迂腐是值得的.:-)一些评论者(除了Daniel Pryden之外)将范围与可达性混淆,或者假设范围内意味着可达性.我甚至听说熟悉这种现象的人说,一个局部变量在最后一次使用后就超出了范围,这当然不是真的. (5认同)
  • `toString()` 语句引起了我的怀疑。由于 `toString()` 实现通常是无副作用的,如果不使用其结果并且 JLS §12.6.1 建议允许此类优化导致更早的收集,则可以完全删除调用。进一步注意,在没有同步的情况下,即使使用像示例打印语句中的结果,也不能保证更长的生命周期。如果没有 *happens-before* 关系,终结器线程可能会看到主线程操作的不同顺序,即在循环完成之前完成 `toString()` 调用。 (3认同)
  • 这个迂腐与我正在调整的东西完全对齐 - 你对范围的使用与我的匹配,我的"`a'被检测为没有引用"匹配你的"可达". (2认同)
  • @Holger严格地说你是对的,`toString()`不提供防止GC的全面保证.但是,对于大多数用途来说,它应该足够好了.在Java 9中有一个新的API确实提供了这样的保证:[`Reference.reachabilityFence`](http://download.java.net/java/jdk9/docs/api/java/lang/ref/Reference.html #reachabilityFence-java.lang.Object-). (2认同)
  • @RFST 从来没有,现在也不能保证。特定的错误报告所解决的所有问题都是 for-each 循环的不可见变量,其中包含可能会阻止收集被垃圾收集的迭代器。正如此答案所示,这通常不适用于优化代码。实际上,我认为这很奇怪,现在`javac` 在每个代码中插入了额外的指令,以解决那个单一的、非常具体的极端情况,当一般行为(悬空局部变量仍然可能存在)没有改变时。 (2认同)

ajb*_*ajb 8

JLS§12.6.1:

可以设计优化程序的转换,以减少可达到的对象的数量,使其少于可以被认为可达的对象的数量.例如,Java编译器或代码生成器可以选择设置将不再用于null的变量或参数,以使得此类对象的存储可能更快地被回收.

所以,是的,我认为这是允许的编译器添加隐藏代码设置anull,从而使其能够进行垃圾回收.如果发生了这种情况,您可能无法从字节码中分辨出来(请参阅@ user2357112的评论).

可能(丑陋)的解决方法:添加public static boolean alwaysFalse = false;到主类或其他类,然后在最后main()添加if (alwaysFalse) System.out.println(a);或引用的其他内容a.我不认为优化器可以肯定地确定alwaysFalse从未设置(因为某些类总是可以使用反射来设置它); 因此,它无法分辨出a不再需要它.至少,这种"解决方法"可用于确定这是否确实是问题.

  • 您无法从字节码中分辨出来,因为转换可能发生在JIT优化中. (4认同)
  • 在main9结束时在Java9中使用`Reference.reachabilityFence(a)`是可靠的,但是如果需要这样的话,它仍然很难看. (4认同)
  • 注意:请注意,它不是`public static**final**boolean alwaysFalse`否则它将是_compile-time constant_,因此在编译时内联(编译器不会为`if`块发出任何代码, JLS) (2认同)
  • 优化器不需要识别`alwaysFalse`是*never*set,它需要知道的是它不是按照程序顺序在`A`的构造和测试之间设置的.由于变量未声明为"volatile",因此优化代码不需要注意其他线程所做的更新,无论是直接还是通过Reflection.但即使它被声明为"volatile",我们也不应该编写代码来解决优化器的假设无能. (2认同)