GC.Collect() and Finalize

46

好的,众所周知,当GC将对象标识为垃圾时,它会隐式调用对象的Finalize方法。但是如果我执行GC.Collect()会发生什么?最终器依然会被执行吗?有人问了我这个问题,我回答了“是的”,然后我想:“那是否完全正确呢?


2
为什么你没有尝试在析构函数中添加日志,以查看是否调用了“Finalize”?还是我漏掉了什么?! - Likurg
13
这绝对不是一个“愚蠢的问题”。 - Eric Lippert
3
@Likurg - 是的,我本可以那样做并避免发布问题。但是这样知识只会限于我一个人。好的一面是,通过在这里发布,我得到了更详细的关于这个主题的答案,我希望它也能帮助其他SO用户! - Sandeep
5个回答

78
好的,众所周知当GC将对象标记为垃圾时,它会隐式调用该对象的Finalize方法。但这不是真实情况,因为为了成为“知识”,一个陈述必须是真实的。该陈述是错误的,垃圾收集器在执行其跟踪过程中,无论是自动运行,还是您调用Collect方法,都不会运行终结器。 终结器线程会在跟踪收集器找到垃圾后异步运行终结器,而这发生在对Collect的调用与此无关。以下是其工作原理的简化概述:
- 当进行一次垃圾收集时,垃圾收集器跟踪线程跟踪根——即已知的存活对象以及它们引用的每个对象,以此类推——来确定死亡对象。 - 具有挂起终结器的“死”对象被移动到终结器队列中。终止器队列是一个根,因此这些“死”对象实际上仍然是存活的。 - 终结器线程通常是与GC跟踪线程不同的线程,最终运行并清空终结器队列。然后这些对象变成真正的死对象,并在下一个跟踪线程的收集中收集它们。(当然,因为它们刚刚幸存第一次收集,所以它们可能在更高的代中。)

就像我说的那样,这有些过于简化了;最终器队列的详细工作方式比这要复杂一些。但它足以传达足够的思想。在这里的实际影响是,你不能假定调用Collect也会运行终结器,因为它不会。让我再重复一遍:垃圾收集器的跟踪部分 不会 运行终结器 ,而Collect仅运行收集机制的跟踪部分。

如果您希望保证所有终结器已运行,请在调用Collect后调用名副其实的WaitForPendingFinalizers。它将暂停当前线程,直到终结器线程完成清空队列。如果您想确保那些已终结的对象已被回收,则需要再次调用Collect 第二次

当然,毋庸置疑,您应该只将此用于调试和测试目的。在没有非常,非常好的理由的情况下,不要在生产代码中执行此类操作。


1
@Sandeep:嗯,我想这取决于你是否认为终结器线程是垃圾收集器的一部分。我认为垃圾收集器特指追踪根的东西,但如果你认为垃圾收集器是处理清理的所有机制,那么我想这是正确的。我会澄清答案。 - Eric Lippert
1
我完全同意。但我只是想知道(出于好奇)-是谁通知终结器线程开始执行的?是GC吗? - Sandeep
3
很高兴在SO上看到你回来了,Eric。 - MgSam
2
@MgSam:谢谢!有些空闲时间可以花在上面真是太好了!我们会看看它能持续多久。 - Eric Lippert
1
@HenkHolterman:不会。在进入终结队列之后,从大对象堆中分配的对象不会进入更高的代,因为 LOH 不是分代的。在经过 2 代之后进入终结队列的对象也不会进入更高的代,因为 2 代是最高的。在非分代版本的 CLR(例如紧凑框架)上进入终结队列的对象也不会进入更高的代。 - Eric Lippert
显示剩余6条评论

15

实际上答案是"这取决于情况"。实际上有一个专门的线程执行所有终结器。这意味着对 GC.Collect 的调用仅触发了此过程,并且执行所有终结器将异步调用。

如果您想等待直到所有终结器都被调用,您可以使用以下技巧:

GC.Collect();
// Waiting till finilizer thread will call all finalizers
GC.WaitForPendingFinalizers();

@OP:有一些原因可能会让你想这样做(例如,作为性能优化)。但是,如果使用这种代码的动机是为了修复程序逻辑,则该代码应该使用 IDisposable.Dispose(最好通过 using 语句)来代替。 - Brian

10

可以,但不是立即进行。这段摘录来自Microsoft .NET Framework的垃圾回收:自动内存管理 (MSDN Magazine) (*)

当应用程序创建一个新对象时,new运算符从堆中分配内存。如果对象的类型包含Finalize方法,则会将指向该对象的指针放置在终结器队列上。终结器队列是由垃圾回收器控制的内部数据结构。队列中的每个条目都指向一个对象,该对象在其内存可以被回收之前应调用其Finalize方法。

当发生GC时,垃圾回收器会扫描终结器队列,寻找指向这些对象的指针。找到指针后,指针将从终结器队列中删除,并附加到可及队列(发音为“F-可达”)。可及队列是另一个由垃圾回收器控制的内部数据结构。可及队列中的每个指针标识一个准备调用其Finalize方法的对象。

有一个专用于调用Finalize方法的特殊运行时线程。当可及队列为空(通常情况下是这样)时,此线程会进入休眠状态。但是,当条目出现时,此线程会醒来,从队列中删除每个条目,并调用每个对象的Finalize方法。因此,您不应在Finalize方法中执行任何假设执行代码的线程的代码。例如,在Finalize方法中避免访问线程本地存储。

(*) 来自2000年11月,所以有些内容可能已经发生变化。


5
当垃圾收集器(无论是响应内存压力还是 GC.Collect())进行垃圾回收时,需要终结的对象会被放入终结队列。
除非您调用 GC.WaitForPendingFinalizers(),否则这些对象的终结器可能会在垃圾回收完成后长时间在后台执行。
顺便说一下,并不保证终结器会被完全调用。从 MSDN 中得知...

以下情况下,终结器可能无法运行到完成或根本不运行:

  • 另一个终结器无限期地阻塞(进入无限循环、尝试获取它永远无法获取的锁等等)。因为运行时试图将终结器运行到完成,所以如果一个终结器无限期地阻塞,则可能不会调用其他终结器。
  • 进程在未给运行时清理的机会的情况下终止。在这种情况下,运行时对进程终止的第一个通知是 DLL_PROCESS_DETACH 通知。

仅当可终结对象数量持续减少时,运行时才会在关闭期间继续终结对象。


0

这里还有几点值得提一下。

Finalizer 是 .net 对象释放非托管资源的最后一步。只有在您没有正确处理实例时,才会执行 Finalizers。理想情况下,在许多情况下都不应该执行 finalizers,因为适当的 dispose 实现应该抑制终结操作

这里有一个正确的 IDispoable 实现示例

如果您调用任何可处置对象的 Dispose 方法,则应清除所有引用并抑制终结操作。如果有任何不太好的开发人员忘记调用 Dispose 方法,则 Finalizer 就是救命稻草。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接