2012-01-11 50 views
12

我在阅读“CLR via C#”,看起来在本例中,最初分配给'obj'的对象在执行第1行之后将有资格进行垃圾收集,而不是第2行。Java与.Net中的对象生命周期

void Foo() 
{ 
    Object obj = new Object(); 
    obj = null; 
} 

这是因为局部变量使用寿命不是由定义的范围定义的,而是由您上次读取的。

所以我的问题是:Java呢?我编写了这个程序来检查这种行为,并且它看起来像对象仍然活着。我不认为JVM在解释字节码时可能会限制变量的生命周期,所以我试图用'java -Xcomp'运行程序来强制编译方法,但'finalize'不会被调用。看起来对于Java来说这不是真的,但我希望我能在这里得到更准确的答案。另外,Android的Dalvik VM呢?

class TestProgram { 

    public static void main(String[] args) { 
     TestProgram ref = new TestProgram(); 
     System.gc(); 
    } 

    @Override 
    protected void finalize() { 
     System.out.println("finalized"); 
    } 
} 

补充: 杰弗里里希特给出了代码示例在 “通过C#CLR”,这样的事情:

public static void Main (string[] args) 
{ 
    var timer = new Timer(TimerCallback, null, 0, 1000); // call every second 
    Console.ReadLine(); 
} 

public static void TimerCallback(Object o) 
{ 
    Console.WriteLine("Callback!"); 
    GC.Collect(); 
} 

TimerCallback对MS的.Net只调用一次,如果项目的目标是 '释放'(定时器在调用GC.Collect()后销毁),并且如果目标是'Debug'(变量寿命增加,因为程序员可以尝试使用调试器访问对象),则每秒调用一次。但是,无论您如何编译它,每秒都会调用Mono回调函数。看起来Mono的'Timer'实现存储对线程池某处实例的引用。 MS实施不这样做。

+10

我以前在Java规范中找不到这个。请注意,.NET可能比你期望的更疯狂 - 当实例方法正在执行时,可能收集实例*如果CLR知道将不会引用更多实例变量。 – 2012-01-11 17:15:02

回答

4

注意,仅仅因为一个对象可以收集,并不意味着它实际上将被收集在任何给定的点 - 所以你的方法可以给假阴性。如果任何对象的finalize方法被调用,你可以肯定地说它是无法访问的,但如果方法没有被调用,你不能从逻辑上推断任何东西。与大多数与GC相关的问题一样,垃圾收集器的非确定性使得很难提出有关它将做什么的测试/保证。

在到达性/可收回的话题,JLS说(12.6.1):

一个可达对象是可以在任何可能继续计算从任何活动线程访问的任何对象。优化程序的转换可以设计为将可达到的对象数量减少到小于天真被认为可达的对象数量。例如,编译器或代码生成器可以选择将不再用于空的变量或参数设置为使得此类对象的存储器可以更快地被回收。

这或多或少是你所期望的正是 - 我觉得上面一段是与同构“的对象是不可达当且仅当你绝对不会用它了。”

回到原来的情况,您能想到在第1行之后被视为无法访问的对象与第2行之间的任何实际影响?我最初的反应是没有,如果你以某种方式设法找到这样的情况,它可能是坏/扭曲代码的标志,导致虚拟机挣扎而不是语言的固有弱点。

虽然我愿意反驳。


编辑:感谢有趣的例子。

我同意你的评估,看看你要去哪里,但问题可能更多的是调试模式微妙地改变了你的代码的语义。

在编写的代码中,您将Timer分配给一个局部变量,该变量随后不会在其范围内进行读取。即使是最平凡的逃逸分析也可以揭示变量在main方法的其他地方没有使用,因此可以省略。因此,我认为你的第一线可以被认为是完全等同于只需要直接调用构造函数:

public static void Main (string[] args) 
{ 
    new Timer(TimerCallback, null, 0, 1000); // call every second 
    ... 

在后一种情况下,很显然,新创建Timer对象是不可达立即施工后(假设它不”不要做任何鬼鬼祟祟的事情,比如将自己添加到静态字段等构造函数中);并且一旦GC收到它,它就会被收集起来。

现在在调试的情况下,事情是微妙的不同,因为你提到的原因是开发人员可能希望稍后在方法中检查局部变量的状态。因此编译器(和JIT编译器)不能优化这些;就好像在方法的末尾有一个变量的访问,直到那个点阻止收集。

即便如此,我不认为这实际上改变了语义。 GC的本质是集合很少得到保证(至少在Java中,唯一的保证是,如果OutOfMemoryError被抛出,那么所有被认为无法访问的东西都会被预先立即调用)。事实上,假设您有足够的堆空间来存放运行时生命周期内创建的每个对象,则无操作的GC实现是完全有效的。因此,虽然您可能会观察到Timer刻度的行为变化,但这没有问题,因为根据调用方式不能保证您会看到什么。 (这在概念上类似于在系统负载情况下运行整个CPU密集型任务的计时器将多次打勾 - 由于该界面不提供这种保证,所以结果都是错误的。)

在这一点我把你引回到这个答案的第一句话。 :)

+0

感谢您的回复。我将编辑我的问题并在那里添加代码示例。 – Ivan 2012-01-11 17:54:18

+1

我在我的文章中添加了代码示例,它显示了不同实现的不同程序行为。网络和不同的构建目标,由该优化引起。 – Ivan 2012-01-11 18:27:38

+0

底层环境中的变量寿命!=语言中的变量,这种行为是不直观的,理论上可以破坏代码,尽管我不认为这确实是一个问题。只是想知道Java规范是否说明了这种情况。使用GC每天看起来越来越复杂:) – Ivan 2012-01-12 16:43:44

1

Java通常具有这样的行为,即如果对象在范围内可访问(存在对该对象的引用而非垃圾),则该对象不是垃圾。这是递归的,所以如果a是对引用b的对象的引用,则对象b指的不是垃圾。

在范围内,你仍然可以达到通过ref引用的对象,(你可以添加一个行System.out.println(ref.toString())),ref是不是垃圾。

但是,根据this old source from Sun's site,大部分依赖于JVM的具体实现。