为什么在对象上调用终结器

时间:2013-12-22 16:46:52

标签: c# garbage-collection finalizer

以下是展示令人惊讶的终结行为的示例程序:

class Something
{
    public void DoSomething()
    {
        Console.WriteLine("Doing something");
    }
    ~Something()
    {
        Console.WriteLine("Called finalizer");
    }
}

namespace TestGC
{
    class Program
    {
        static void Main(string[] args)
        {
           var s = new Something();
           s.DoSomething();
           GC.Collect();
           //GC.WaitForPendingFinalizers();
           s.DoSomething();
           Console.ReadKey();
        }
    }
}

如果我运行该程序,打印的内容是:

Doing something
Doing something
Called finalizer

这看起来像预期的那样。因为在调用GC.Collect()之后存在对s的引用,所以s不是垃圾。

现在从//GC.WaitForPendingFinalizers();行删除评论 再次构建并运行程序。

我希望输出中没有任何改变。这是因为我读到如果发现对象是垃圾并且它有终结器,它将被放在终结器队列上。由于对象不是垃圾,因此它不应该放在终结器队列上似乎是合乎逻辑的。因此,注释掉的那条线应该什么都不做。

但是,该程序的输出是:

Doing something
Called finalizer
Doing something

有人可以帮助我理解为什么终结器会被调用吗?

3 个答案:

答案 0 :(得分:7)

我无法在笔记本电脑上重现此问题,但您的DoSomething方法不会使用对象中的任何字段。这意味着即使DoSomething正在运行,也可以最终确定对象

如果您将代码更改为:

class Something
{
    int x = 10;

    public void DoSomething()
    {
        x++;
        Console.WriteLine("Doing something");
        Console.WriteLine("x = {0}", x);
    }
    ~Something()
    {
        Console.WriteLine("Called finalizer");
    }
}

...然后我怀疑你将总是在“Called finalizer”之前看到DoingSomething两次打印 - 尽管最后的“x = 12”可能会在“Called finalizer”之后打印出来。

基本上,最终确定可能有点令人惊讶 - 我非常很少发现自己使用它,并鼓励你尽可能避免使用终结器。

答案 1 :(得分:7)

Jon的回答当然是对的。我想补充一点,C#规范要求编译器和运行时允许(但不是必需)注意到局部变量中包含的引用永远不会再次取消引用,并且在那时,如果本地是最后的生命引用,则允许垃圾收集器将对象视为死亡。因此,即使在生活局部变量中似乎存在引用,也可以收集对象并且终结器运行。 (同样,如果他们愿意,编译器和运行时允许本地人更长。)

鉴于这一事实,你最终可能会陷入奇怪的境地。例如,终结器可以在终结器线程上执行,而对象的构造函数在用户线程上运行。如果运行时可以确定“this”永远不再被解除引用,那么在构造函数完成“this”字段变异的那一刻,该对象可以被视为死。如果构造函数然后执行其他工作 - 比如改变全局状态 - 那么可以在终结器运行时完成该工作。

这也是为什么编写正确的终结器非常困难的另一个原因,你可能不应该这样做。在结束语 everything you know is wrong 中。你所指的一切都可能已经死了,你在一个不同的线程,对象可能没有完全构造,可能没有你的程序不变量实际维护。

答案 2 :(得分:3)

你的DoSomething()是如此微不足道,以至于它可能会被内联。在内联之后,没有任何东西仍然可以引用该对象,因此没有什么可以防止它被垃圾收集。

GC.KeepAlive()专为此方案而设计。如果要防止对象被垃圾回收,可以使用它。它什么都不做,但垃圾收集器不知道。在GC.KeepAlive(s);结束时致电Main,以防止它提前完成。