【问题标题】:Why is finalizer called on object为什么在对象上调用终结器
【发布时间】:2013-12-22 16:46:52
【问题描述】:

这是一个展示令人惊讶的终结行为的示例程序:

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(); 行中删除 cmets 再次构建并运行程序。

我希望输出不会发生任何变化。这是因为我读到如果发现对象是垃圾并且它有一个终结器,它将被放入终结器队列。由于对象不是垃圾,因此不应将其放入终结器队列似乎是合乎逻辑的。因此,注释掉的行应该什么都不做。

但是,程序的输出是:

Doing something
Called finalizer
Doing something

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

【问题讨论】:

    标签: c# garbage-collection finalizer


    【解决方案1】:

    我无法在我的笔记本电脑上重现该问题,但您的 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");
        }
    }
    

    ...那么我怀疑你会总是看到 DoingSomething 在“Called finalizer”之前打印两次 - 尽管最终的“x = 12”可能会在“Called finalizer”之后打印。 p>

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

    【讨论】:

    • 确实,这有所作为。谢谢。现实生活中的例子是更多涉及到我需要终结器的 C++/CLI 程序。
    • @Tony:您对自己真的需要终结器的信心如何?例如,请参阅webcache.googleusercontent.com/…
    【解决方案2】:

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

    鉴于这一事实,您最终可能会陷入奇怪的境地。例如,终结器可能在终结器线程上执行而对象的构造函数在用户线程上运行。如果运行时可以确定“this”不再被取消引用,那么在构造函数完成对“this”字段的变异时,该对象可以被视为已死。如果构造函数随后做了额外的工作——比如改变全局状态——那么这项工作可以在终结器运行时完成。

    这只是编写正确的终结器如此困难以至于您可能不应该这样做的另一个原因。在终结器 everything you know is wrong 中。您所指的所有内容都可能已失效,您在不同的线程上,对象可能未完全构造,可能实际上没有维护您的程序不变量。

    【讨论】:

    • 更糟糕的是,当一个对象不存在强根引用时,它可能有资格立即完成,但要重新建立强根引用,以及其他代码在终结器运行之前开始使用对象。即使物体本身不会复活,物体也很难防备被别的东西复活。
    【解决方案3】:

    您的DoSomething() 非常琐碎,很可能会被内联。在它被内联之后,没有任何东西仍然具有对该对象的引用,因此没有任何东西阻止它被垃圾收集。

    GC.KeepAlive() 专为这种情况而设计。如果您想防止对象被垃圾收集,则可以使用它。它什么也不做,但垃圾收集器不知道这一点。在Main 末尾调用GC.KeepAlive(s); 以防止它提前完成。

    【讨论】:

      猜你喜欢
      • 2017-07-08
      • 1970-01-01
      • 2010-10-11
      • 1970-01-01
      • 1970-01-01
      • 2017-05-11
      • 1970-01-01
      • 2010-09-26
      • 1970-01-01
      相关资源
      最近更新 更多