Java和.NET中对象的生命周期

11
我正在阅读《CLR via C#》,据此例子,被最初分配给“obj”的对象将在执行第一行之后,而不是第二行之后,有资格进行垃圾回收。
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");
    }
}

新增: Jeffrey Richter 在《CLR via C#》一书中提供了代码示例,例如:

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();
}

如果项目的目标是'Release',则在MS .Net上TimerCallback仅调用一次(在GC.Collect()调用后计时器被销毁),如果目标为'Debug',则每秒钟调用一次(变量生命周期增加,因为程序员可以尝试使用调试器访问对象)。但是在Mono上,无论你如何编译,回调都会每秒钟调用一次。看起来Mono的'Timer'实现在线程池中某个地方存储了对实例的引用。而MS的实现则不这样做。


10
在Java规范中我未能找到这个内容。需要注意的是,.NET可能比你想象的更疯狂 - 如果CLR知道不再引用任何实例变量,甚至可能在执行实例方法时收集一个实例。 - Jon Skeet
2个回答

3
请注意,仅因为对象可以被收集并不意味着它在任何给定时刻都会被收集 - 因此您的方法可能会产生错误的负面结果。如果调用了任何对象的finalize方法,则可以确定它是不可达的,但如果未调用该方法,则不能从逻辑上推断出任何内容。与大多数垃圾回收相关的问题一样,垃圾收集器的非确定性使得难以编写关于其确切行为的测试/保证。
关于可达性/可收集性,JLS说(12.6.1):
“可达对象是任何可以从任何活动线程中访问的对象。可以设计优化程序的转换,以将可达对象的数量减少为比那些可能被认为是可达的对象要少。例如,编译器或代码生成器可以选择将不再使用的变量或参数设置为null,以便更早地释放该对象的存储。”
这几乎是您期望的内容-我认为上面的段落与“如果您肯定不再使用对象,则该对象是不可达的”等效。
回到您最初的情况,您是否能想到在第一行和第二行之后被视为不可达对象之间的任何实际影响? 我的初始反应是没有,如果您设法找到这种情况,它可能是代码错误/扭曲导致VM挣扎的标志,而不是语言固有的弱点。
虽然我愿意听取反驳观点。
编辑:感谢您提供有趣的示例。
我同意您的评估并理解您的想法,尽管问题可能更多地在于调试模式正在微妙地改变代码的语义。
在编写的代码中,您将计时器分配给本地变量,该变量在其范围内未被随后读取。即使进行最简单的逃逸分析,也可以发现timer变量在main方法中没有任何其他用途,因此可以省略。因此,我认为第一行与直接调用构造函数完全等效:
public static void Main (string[] args)
{
    new Timer(TimerCallback, null, 0, 1000); // call every second
    ...

在后一种情况下,很明显新创建的Timer对象在构造之后不会立即可达(假设它在构造函数中没有添加自身到静态字段等这样的花里胡哨的操作),因此只要垃圾回收机制遍历到它,它就会被回收。
现在,在调试情况下,由于开发人员可能希望稍后检查方法中的局部变量状态,事情就有了微妙的不同。因此,编译器(包括JIT编译器)无法优化掉这些变量;好像在方法末尾访问了该变量,从而防止直到那时才进行回收。
即便如此,我认为这并不会实际改变语义。GC的本质是很少能够保证回收(至少在Java中,您唯一得到的保证是如果抛出OutOfMemoryError,则在此之前被视为不可达的所有内容都会被立即回收)。事实上,假设您拥有足够的堆空间来容纳运行时期间创建的每个对象,则空操作的GC实现是完全有效的。因此,虽然您可能会观察到Timer ticks的次数有所变化,但由于根据您调用它的方式没有任何保证,因此这是可以接受的。(这在概念上类似于在CPU密集型任务期间运行的计时器,当系统负载较大时,它会跳动更多次 - 由于接口不提供这种类型的保证,因此两种结果都不会出错。)
此时,我引用本回答中的第一句话。 :)

感谢您的回复。我会编辑我的问题,在那里添加代码示例。 - user1143634
1
我已经在我的帖子中添加了代码示例,它展示了在不同的 .Net 实现和不同的构建目标上由于优化而导致的不同程序行为。 - user1143634
底层环境中的变量寿命与语言中的变量寿命不同,我认为这种行为很不直观,在理论上可能会破坏代码,但我认为这并不是真正的问题。只是想知道Java规范是否对这种情况有规定。使用GC看起来变得越来越复杂了 :) - user1143634

1

Java通常具有这样的行为,如果对象在作用域内是可达的(存在一个不是垃圾的引用),那么该对象就不是垃圾。这是递归的,因此如果a是对具有对b的引用的对象的引用,则b所引用的对象不是垃圾。

在您仍然可以访问ref引用的对象的范围内(您可以添加一行System.out.println(ref.toString())),ref不是垃圾。

但是,根据Sun网站上的旧来源,大多数情况取决于JVM的具体实现。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接