以下是展示令人惊讶的终结行为的示例程序:
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
有人可以帮助我理解为什么终结器会被调用吗?
答案 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
,以防止它提前完成。