3

析构函数应该只释放你的对象持有的非托管资源,它不应该引用其他对象。如果您只有托管引用,则不需要(也不应该)实现析构函数。您只希望它用于处理非托管资源。因为有一个析构函数需要一些成本,所以你应该只在消耗有价值的、非托管资源的方法上实现它。

-- C++ 程序员的 C# 十大陷阱

本文没有对此进行更深入的讨论,但是在 C# 中使用析构函数会涉及哪些成本?

注意:我知道 GC 和析构函数在可靠时间没有被调用的事实,除此之外,还有什么其他的吗?

4

4 回答 4

8

任何具有终结器的对象(我更喜欢这个术语而不是析构函数,以强调与 C++ 析构函数的区别)被添加到终结器队列中。这是对具有终结器的对象的引用列表,在删除它们之前必须调用该终结器。

当对象准备好进行垃圾回收时,GC 会发现它在终结器队列中,并将引用移动到可访问(f-reachable)队列。这是终结器后台线程依次调用每个对象的终结器方法所经过的列表。

一旦调用了对象的终结器,该对象就不再在终结器队列中,因此它只是一个常规的托管对象,GC 可以删除它。

这一切都意味着,如果一个对象有一个终结器,它至少会在一次垃圾回收中存活下来,然后才能被删除。这通常意味着对象将被移动到下一个堆代,这实际上涉及将内存中的数据从一个堆移动到另一个堆。

于 2009-03-05T01:51:49.023 回答
6

我见过的关于这一切如何运作的最广泛的讨论是由 Joe Duffy 完成的。它的细节比你想象的要多。

在此之后,我每天都制定了一种实用的方法来执行此操作 - 更少关注成本,更多关注实施。

于 2009-03-05T01:45:49.443 回答
3

Guffa 和 JaredPar 很好地涵盖了细节,所以我将添加一些关于终结器或析构函数的有点深奥的注释,不幸的是 C# 语言规范称之为它们。

要记住的一件事是,由于终结器线程按顺序运行所有终结器,终结器中的死锁将阻止所有剩余(和未来)终结器运行。由于这些实例在它们的终结器完成之前不会被收集,因此死锁的终结器也会导致内存泄漏。

于 2009-03-05T02:24:10.523 回答
0

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 从终结器队列中删除对象是将对象留在队列中的一半成本。

于 2009-03-05T04:58:17.860 回答