析构函数应该只释放你的对象持有的非托管资源,它不应该引用其他对象。如果您只有托管引用,则不需要(也不应该)实现析构函数。您只希望它用于处理非托管资源。因为有一个析构函数需要一些成本,所以你应该只在消耗有价值的、非托管资源的方法上实现它。
本文没有对此进行更深入的讨论,但是在 C# 中使用析构函数会涉及哪些成本?
注意:我知道 GC 和析构函数在可靠时间没有被调用的事实,除此之外,还有什么其他的吗?
析构函数应该只释放你的对象持有的非托管资源,它不应该引用其他对象。如果您只有托管引用,则不需要(也不应该)实现析构函数。您只希望它用于处理非托管资源。因为有一个析构函数需要一些成本,所以你应该只在消耗有价值的、非托管资源的方法上实现它。
本文没有对此进行更深入的讨论,但是在 C# 中使用析构函数会涉及哪些成本?
注意:我知道 GC 和析构函数在可靠时间没有被调用的事实,除此之外,还有什么其他的吗?
任何具有终结器的对象(我更喜欢这个术语而不是析构函数,以强调与 C++ 析构函数的区别)被添加到终结器队列中。这是对具有终结器的对象的引用列表,在删除它们之前必须调用该终结器。
当对象准备好进行垃圾回收时,GC 会发现它在终结器队列中,并将引用移动到可访问(f-reachable)队列。这是终结器后台线程依次调用每个对象的终结器方法所经过的列表。
一旦调用了对象的终结器,该对象就不再在终结器队列中,因此它只是一个常规的托管对象,GC 可以删除它。
这一切都意味着,如果一个对象有一个终结器,它至少会在一次垃圾回收中存活下来,然后才能被删除。这通常意味着对象将被移动到下一个堆代,这实际上涉及将内存中的数据从一个堆移动到另一个堆。
我见过的关于这一切如何运作的最广泛的讨论是由 Joe Duffy 完成的。它的细节比你想象的要多。
在此之后,我每天都制定了一种实用的方法来执行此操作 - 更少关注成本,更多关注实施。
Guffa 和 JaredPar 很好地涵盖了细节,所以我将添加一些关于终结器或析构函数的有点深奥的注释,不幸的是 C# 语言规范称之为它们。
要记住的一件事是,由于终结器线程按顺序运行所有终结器,终结器中的死锁将阻止所有剩余(和未来)终结器运行。由于这些实例在它们的终结器完成之前不会被收集,因此死锁的终结器也会导致内存泄漏。
Guffa已经很好地总结了终结器成本的因素。最近有一篇关于 Java 中终结器成本的文章也提供了一些见解。
通过使用 GC.SuppressFinalize 从终结器队列中删除对象,可以避免 .net 中的部分成本。我根据文章在 .net 中进行了一些快速测试,并将其发布在此处(尽管重点更多地放在 Java 方面)。
下面是结果图 - 它实际上并没有最好的标签;-)。“Debug=true/false”指的是空的 vs 简单的终结器:
~ConditionalFinalizer()
{
if (DEBUG)
{
if (!resourceClosed)
{
Console.Error.WriteLine("Object not disposed");
}
resourceClosed = true;
}
}
“Suppress=true”是指在 Dipose 方法中是否调用了 GC.SuppressFinalize。
对于 .net,通过调用 GC.SuppressFinalize 从终结器队列中删除对象是将对象留在队列中的一半成本。