垃圾收集和终结器:更好的点

时间:2011-03-07 18:00:05

标签: c# garbage-collection finalizer

在SO上回答另一个问题*以及随后的评论讨论时,我遇到了一个我不清楚的问题。

在我误入歧途的任何地方纠正我......

当垃圾收集器收集一个对象时,它会在一个单独的线程上调用该对象的终结器(除非终结器已被抑制,例如通过Dispose()方法)。在收集时,GC会挂起除触发集合的线程(除了背景集合)之外的所有线程。

不清楚的是:

  1. 在收集垃圾收集器之前,垃圾收集器是否等待终结器在该对象上执行?
  2. 如果没有,是否在终结器仍在执行时取消挂起线程?
  3. 如果它确实等待,如果终结器遇到其中一个被挂起的线程持有的锁,会发生什么?终结器线程是否会死锁? (在我的回答中,我认为这是糟糕的设计,但我可能会看到可能发生这种情况的情况)
  4. *链接到原始问题:
    .NET GC Accessing a synchronised object from a finalizer

3 个答案:

答案 0 :(得分:46)

  

在收集垃圾收集器之前,垃圾收集器是否等待终结器在该对象上执行?

你的问题有点含糊不清。

当GC遇到需要完成的“死”对象时,它放弃了回收死对象存储的尝试。相反,它将对象放在“我知道需要最终化的对象”的队列中,并且将该对象视为活着,直到终结器线程完成它为止。

所以,是的,GC确实“等待”,直到在回收存储之前执行终结器。但它不会等待同步。听起来你问“GC是否同步调用终结器?”不,它将对象排队等待稍后完成并继续卡车运行。 GC希望快速完成释放垃圾和压缩内存的任务,以便程序能够尽快恢复运行。在清理之前,它不会停止处理一些需要注意的怪物。它将该对象放在队列中并说“保持安静,终结器线程将在稍后处理你”。

稍后GC会再次检查对象并说“你还在死吗?你的终结器已经运行了吗?”如果答案是“是”,那么对象将被回收。 (请记住,终结者可能会将一个死对象重新变为现实对象;尽量不要这样做。结果没有任何令人愉快的事情发生。)

  

在终结器仍在执行时是否取消挂起线程?

我相信GC解冻了它冻结的线程,并向终结器线程发出信号“嘿,你有工作要做”。因此,当终结器线程开始运行时,GC冻结的线程将再次启动。

可能必须有未冻结的线程,因为终结器可能需要对用户线程进行编组调用才能释放线程关联资源。当然,某些用户线程可能被阻止或冻结;线程总是被某些东西阻挡。

  

如果终结器遇到其中一个被挂起的线程所持有的锁,会发生什么?终结器线程是否会死锁?

你打赌。终结器线程没有任何魔力可以阻止它死锁。如果用户线程正在等待终结器线程取出的锁,并且终结器线程正在等待用户线程取出的锁,那么你就会遇到死锁。

终结器线程死锁的例子比比皆是。这是一篇关于其中一个场景的好文章,其中包含一系列指向其他场景的链接:

http://blogs.microsoft.co.il/blogs/sasha/archive/2010/06/30/sta-objects-and-the-finalizer-thread-tale-of-a-deadlock.aspx

正如文章所述:终结器是一种极其复杂和危险的清理机制,如果你可以,你应该避免使用它们。让终结器出错是非常容易的,并且很难做到正确。

答案 1 :(得分:4)

包含终结器的对象往往寿命更长。在收集期间,当GC将终结器标记为垃圾时,它将不会收集该对象(尚未)。 GC会将该对象添加到将在 GC完成后运行的终结器队列。这样做的后果是,因为没有收集这个对象,所以它会移动到下一代(以及它所引用的所有对象)。

GC挂起所有正在运行的线程。另一方面,终结器线程将在应用程序继续运行时在后台运行。终结器调用所有注册用于完成的对象的所有finalize方法。在对象的终结器方法运行之后,该对象将从队列中删除,并且从对象上的那个点(可能还有它仍然引用的所有对象)都是垃圾。清除该对象生成对象的下一个集合将(最后)删除该对象。由于生成在第2代中的对象的收集速度是生成第1代的对象的10倍,而第1代收集的速度是第0代的10倍,因此最终可能需要花费一些时间来对这些对象进行垃圾收集。

因为终结器线程只是一个运行托管代码的简单线程(它调用终结器),所以它可以阻塞甚至死锁。因此,在finalize方法中尽可能少地做很重要。因为终结器是后台线程,失败的终结方法甚至可以打倒完整的AppDomain(哎呀!)。

你可以说这个设计是不幸的,但如果你考虑一下,其他设计框架有效清理我们的混乱,很难想象。

所以,回答你的问题:

  1. 是的,只有在从终结器队列中删除对象后,该对象才会是垃圾,GC会收集它。
  2. GC挂起所有线程,甚至是终结器队列。
  3. 终结器队列可能会死锁。在finalize方法中尽可能少地锁定。

答案 2 :(得分:3)

最简单的想法是将垃圾收集器视为将对象分为四组:

  1. 任何有根对象无法访问的内容;
  2. 可从可实现的可终结对象列表中访问的对象,但不能从任何其他有根对象访问;
  3. 那些在可实现的可终结对象列表中的内容,但也可通过该列表以外的某个有根对象访问。
  4. 那些不在实时可终结对象列表中的,但可以通过该列表以外的某个有根对象访问。
  5. 当垃圾收集器运行时,#1类型的对象消失。 #2的对象被添加到需要即将完成的对象列表中,并从“可实现的可终结对象”列表中删除(从而成为类别#4的对象)。请注意,需要最终确定的对象列表是一个普通的有根引用,因此当列表上的对象不能被收集时,如果在终结器完成时没有创建其他有根引用,则对象将移动到类别# 1。