16

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

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

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

什么不清楚:

  1. 垃圾收集器在收集之前是否等待终结器在该对象上执行?
  2. 如果不是,它是否在终结器仍在执行时取消挂起线程?
  3. 如果它确实等待,如果终结器遇到被挂起线程之一持有的锁会发生什么?终结器线程是否死锁?(在我的回答中,我认为这是一个糟糕的设计,但我可能会看到可能发生这种情况的情况)

* 链接到原始问题:
.NET GC Accessing a synchronized object from a finalizer

4

3 回答 3

53

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

你的问题有点模棱两可。

当 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

正如文章所述:终结器是一种极其复杂且危险的清理机制,如果可能,您应该避免使用它们。将终结器弄错非常容易,而要让它正确则非常困难。

于 2011-03-07T18:27:30.937 回答
4

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

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

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

你可以说这种设计是不幸的,但如果你仔细想想,其他框架能有效清理我们的烂摊子的设计,是很难想象的。

所以,回答你的问题:

  1. 是的,只有在对象从终结器队列中移除后,该对象才会成为垃圾,GC 会收集它。
  2. GC 挂起所有线程,甚至是终结器队列。
  3. 终结器队列可能会死锁。在 finalize 方法中尽可能少地锁定。
于 2011-03-07T18:30:45.900 回答
3

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

  1. 任何根对象都无法访问的那些;
  2. 那些可以从实时可终结对象列表中访问的对象,但不能从任何其他根对象中访问;
  3. 那些在实时可终结对象列表中的对象,但也可以通过该列表以外的某个根对象访问。
  4. 那些不在实时可终结对象列表中,但可以通过该列表以外的某个根对象访问的对象。

当垃圾收集器运行时,#1 类型的对象消失。#2 的对象被添加到需要立即终结的对象列表中,并从“实时可终结对象”列表中删除(因此成为类别 #4 的对象)。请注意,需要终结的对象列表是一个普通的根引用,因此无法收集此列表上的对象,但如果在终结器完成时没有创建其他根引用,则该对象将移至类别# 1.

于 2011-03-07T18:42:59.213 回答