【问题标题】:Objects lifespan in Java vs .NetJava与.Net中的对象寿命
【发布时间】:2012-01-11 17:13:15
【问题描述】:

我正在阅读“通过 C# 进行 CLR”,似乎在此示例中,最初分配给“obj”的对象将在执行第 1 行而不是第 2 行之后有资格进行垃圾回收。

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”(在 GC.Collect() 调用后计时器被销毁),TimerCallback 在 MS .Net 上仅调用一次,如果目标是“调试”,则每秒调用一次(变量寿命增加,因为程序员可以尝试访问带有调试器的对象)。但是无论您如何编译它,都会每秒调用 Mono 回调。看起来 Mono 的“计时器”实现在线程池中的某处存储了对实例的引用。 MS 实现不这样做。

【问题讨论】:

  • 我之前在 Java 规范中找不到这个。请注意,.NET 可能比您预期的更疯狂 - 如果 CLR 知道不会再引用更多实例变量,那么在执行实例方法时可能会收集实例。

标签: c# java android .net garbage-collection


【解决方案1】:

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

关于可达性/可收集性的话题,JLS 说 (12.6.1):

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

这或多或少正是你所期望的——我认为上面的段落与“如果你绝对不再使用一个对象是不可访问的”是同构的。

回到你原来的情况,你能想到在第 1 行和第 2 行之后被认为不可访问的对象之间的任何实际后果?我最初的反应是没有,如果你以某种方式设法找到这种情况,它可能是导致 VM 出现问题的错误/扭曲代码的标志,而不是语言的固有弱点。

虽然我愿意接受反驳。


编辑:感谢这个有趣的例子。

我同意你的评估,看看你的目标,虽然问题可能更多的是调试模式正在巧妙地改变你代码的语义。

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

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

在后一种情况下,很明显新创建的Timer 对象在构造后无法立即访问(假设它没有做任何偷偷摸摸的事情,比如在其构造函数中将自己添加到静态字段等);这样它就会在 GC 找到它时立即被收集。

现在在调试情况下,情况略有不同,因为您提到的原因是开发人员可能希望稍后在方法中检查局部变量的状态。因此编译器(和 JIT 编译器)无法优化这些;就好像在方法结束时可以访问变量一样,直到那时才进行收集。

即便如此,我认为这实际上并没有改变语义。 GC 的本质是很少保证收集(至少在 Java 中,你得到的唯一保证是,如果抛出 OutOfMemoryError ,那么所有被认为不可达的东西都会事先立即被 GC)。事实上,假设您有足够的堆空间来容纳在运行时生命周期内创建的每个对象,那么无操作 GC 实现是完全有效的。因此,虽然您可能会观察到 Timer 滴答声的次数发生了行为变化,但这很好,因为无法保证根据您调用它的方式您会看到什么。 (这在概念上类似于在整个 CPU 密集型任务中运行的计时器在系统处于负载下时会计时更多次 - 两种结果都不是错误的,因为接口不提供这种保证。)

在这一点上,我建议您回到此答案中的第一句话。 :)

【讨论】:

  • 感谢您的回复。我将编辑我的问题并在那里添加代码示例。
  • 我已经在我的帖子中添加了代码示例,它显示了不同的 .Net 实现和不同构建目标上的不同程序行为,这是由该优化引起的。
  • 底层环境中的可变寿命!=对于语言中的那个,imo这种行为是不直观的,理论上可能会破坏代码,尽管我认为这不是一个真正的问题。只是想知道 Java 规范是否对这种情况有所说明。每天使用 GC 看起来越来越复杂 :)
【解决方案2】:

Java,一般来说,如果对象在范围内是可访问的(有一个不是垃圾的引用),那么这个对象就不是垃圾。这是递归的,所以如果a 是对一个对象的引用,而该对象又引用了b,那么b 所引用的对象就不是垃圾。

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

不过根据this old source from Sun's site的说法,大部分还是要看JVM的具体实现。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-10-14
    • 1970-01-01
    • 1970-01-01
    • 2011-03-21
    • 2021-07-03
    • 2016-01-12
    • 1970-01-01
    相关资源
    最近更新 更多