问题 特殊情况寿命分析


假设我有

void foo () {
    Bar bar = new Bar(); // bar is never referred to after this line
    // (1)
    doSomethingWithoutBar();
}

在(1),是对象 bar 指着 合格 垃圾收集?或者 bar 还必须超出范围?它是否有所作为 GC.Collect 被称为 doSomethingWithoutBar

这与知道Bar是否有(C#)析构函数或者像这样的类似的东西有关。


10879
2017-07-13 21:52


起源

我要等到埃里克·利珀特(Eric Lippert)来到这里并照顾这一个...... - Stefan H
@StefanH,我想我已经看到Eric在一个非常类似的问题(或者是他的博客文章)上给出了答案但是我的google-fu现在失败了=( - Rob
@Rob - 我知道!我试过谷歌搜索“C#垃圾收集”,因为我认为他将是第一个结果。 - Stefan H
仅供参考,在这些情况下(或更常见的是,当非托管资源需要保持活力时)的常见做法是使用 GC.KeepAlive()。 - Rei Miyasaka


答案:


一旦确定不再使用对象,对象就有资格进行垃圾收集。这完全有可能 bar 将在变量超出范围之前进行垃圾收集。

证明:

using System;

class Bar
{
    ~Bar() { Console.WriteLine("Finalized!"); }
}

class Program
{
    static void Main(string[] args)
    {
        Bar bar = new Bar();
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Press any key to exit...");
        Console.ReadLine();
    }
}

跑进 发布模式 (因为它没有在调试模式下收集)。

输出:

敲定!
按任何一个键退出...

它也适用 ideone 它使用Mono。输出是一样的。


8
2017-07-13 21:57



我认为它将等待范围完成,以确保没有任何 运行 引用该本地对象。 - Tigran
@Tigran,因为它是一个局部变量,怎么可能有任何参考 运行 对吗? =) - Rob
我无法重现这种情况。在任何版本的框架中:2,3.0,3.5,4.0。 - Hailton
@Hailton:您是否尝试在发布模式下运行它?我在发布模式下使用Visual Studio C#2010 Express(C#4)对其进行了测试。您可以单击ideone链接并查看输出 - 它也可以在那里工作。我也稍微简化了代码。您只需将其复制并粘贴到新的控制台项目中,切换到“释放”模式,然后按F5即可。我使用这些说明将Visual Studio Express更改为在发布模式下构建: stackoverflow.com/a/3127008/61974 - Mark Byers
你是对的。在发布模式中,我能够重现这种情况。谢谢你的澄清。我有一个问题:我制作了类似的代码但不能用GC来收集对象。我将使用此代码发布一个新问题。 - Hailton


从快速阅读规范看,它看起来像是特定的实现。它的 允许 垃圾收集它,但不是 需要 至。

我从第10.9节“自动内存管理”中的说明中得到了这个 ECMA规范

[注意:实现可能会选择分析代码来确定   对象的引用可以在将来使用。对于   例如,如果范围内的局部变量是唯一存在的   引用一个对象,但永远不会引用该局部变量   在当前执行的任何可能的继续执行中   在程序中指出, 实施可能(但不是必需的)   to)将对象视为不再使用。结束说明]

强调我的。


3
2017-07-13 22:01





如果没有定义您所指的CLR的版本,那就是 不可能很难确定你在这里看到的行为。

一个 假想 在此示例中,CLR可以假设以下情况属实:

  1. 的构造函数 Bar 什么也没做
  2. 没有初始化的字段(即对象构造没有潜在的副作用)

完全无视这条线 Bar bar = new Bar(); 因为它“无所事事”而优化它。

就我的记忆而言,在当前版本的CLR中 bar 在构建之后,有资格进行垃圾收集。


1
2017-07-13 22:01



我认为你的意思是“假设的编译器” - Stefan H
@StefanH,如果用“假设的编译器”你的意思是“假设的 JIT 编译器“,然后是的,这就是我的意思=)否则 Bar 可以在不同的组件中定义,可以在后面换出csc 编译,导致 Bar 在编译时被剥离,即使 Bar 在运行时会做一些事情..(认为加载项开发是一种可能的场景) - Rob
我更想到它会在CLR创建之前被删除,这是在编译时,对吧?编辑:不,我错了(CLR并不意味着我的意思),我认为它可能会在生成CIL之前被剥离。 - Stefan H
CLR =公共语言运行时,即运行csc / vbc编译器生成的MSIL / CIL的“.net虚拟机”。在我的假设场景中, Bar 可以在不同的程序集中定义,因此编译器无法将其删除,因为它无法确定程序集包含 Bar 在运行时将具有相同的实现 Bar。只有JIT编译器才能知道=) - Rob
谢谢,这很有道理。我很欣赏这个解释。在将来,我可能会说“我只是等待Rob来到这里照顾这个......” - Stefan H


马克回答了这个问题,但这是解决方案:

void foo () {
    Bar bar = new Bar(); // bar is never referred to after this line
    // (1)
    doSomethingWithoutBar();

    GC.KeepAlive(bar); // At the point where you no longer need it
}

1
2017-07-13 22:36





这肯定会发生。例如,这是一个演示,当您仍在执行其构造函数时,可以最终确定实例:

class Program
{
    private static int _lifeState;
    private static bool _end;

    private sealed class Schrodinger
    {
        private int _x;

        public Schrodinger()
        {
            //Here I'm using 'this'
            _x = 1;

            //But now I no longer reference 'this'
            _lifeState = 1;

            //Keep busy to provide an opportunity for GC to collect me
            for (int i=0;i<10000; i++)
            {
                var garbage = new char[20000];
            }

            //Did I die before I finished being constructed?
            if (Interlocked.CompareExchange(ref _lifeState, 0, 1) == 2)
            {
                Console.WriteLine("Am I dead or alive?");
                _end = true;
            }
        }

        ~Schrodinger()
        {
            _lifeState = 2;
        }
    }

    static void Main(string[] args)
    {
        //Keep the GC churning away at finalization to demonstrate the case
        Task.Factory.StartNew(() =>
            {
                while (!_end)
                {
                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                }
            });

        //Keep constructing cats until we find the desired case
        int catCount = 0;
        while (!_end)
        {
            catCount++;
            var cat = new Schrodinger();
            while (_lifeState != 2)
            {
                Thread.Yield();
            }
        }
        Console.WriteLine("{0} cats died in the making of this boundary case", catCount);
        Console.ReadKey();
    }
}

为了使其工作,您需要发出Release版本并在Visual Studio外部运行它(否则调试器会插入阻止该效果的代码。)我已经使用VS 2010目标.NET 4.0 x64对此进行了测试。

您可以在“保持忙碌”循环中调整迭代,以影响Cat完成构建之前完成的清理概率。


1
2017-07-13 22:55