【问题标题】:Garbage Collection and Finalizers: Finer Points垃圾收集和终结器:更精细的点
【发布时间】:2011-03-07 18:00:05
【问题描述】:

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

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

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

不清楚的地方:

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

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

【问题讨论】:

    标签: c# garbage-collection finalizer


    【解决方案1】:

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

    你的问题有点含糊。

    当 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 终结器是一种极其复杂和危险的清理机制,如果可能,您应该避免使用它们
    • "并且在终结器线程处理完它之前将该对象视为活动对象" 在该上下文中,终结器队列不只是算作对该对象的另一个引用,所以它会属于仅 GC'ing 不再引用的对象的正常规则吗?
    • 对于“如果终结器遇到被挂起的线程之一持有的锁会发生什么?”的问题,答案不是“GC 在完成收集时取消挂起所有线程” ?
    • @BrokenGlass:正确;终结器队列只是另一个“保证活着”的根对象。当终结器线程从队列中删除对象时,GC 最终将再次发现它已经死了,当然,除非正如我所说,终结器通过将对象的引用复制到静态字段中来复活对象。跨度>
    • @Gabe:对,GC 解冻了 GC 冻结的线程。我将这个问题解释为“当终结器线程需要访问已被挂起/阻塞线程锁定的资源时会发生什么?”这个更普遍的问题。谁暂停它真的无关紧要。这会让你陷入僵局。
    【解决方案2】:

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

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

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

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

    所以,回答你的问题:

    1. 是的,只有在对象从终结器队列中移除后,该对象才会成为垃圾,GC 会将其回收。
    2. GC 挂起所有线程,包括终结器队列。
    3. 终结器队列可能死锁。在 finalize 方法中尽可能少地锁定。

    【讨论】:

    • 我并不是说垃圾收集器的设计是不幸的……有问题的原始程序的设计是不幸的:)
    【解决方案3】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-14
      • 2015-01-06
      • 1970-01-01
      • 1970-01-01
      • 2018-12-30
      • 1970-01-01
      相关资源
      最近更新 更多