C#析构函数(又称:终结器)相关的成本是什么?

3
析构函数只应该释放对象持有的非托管资源,并且不应引用其他对象。如果您只有托管引用,则不需要(也不应)实现析构函数。您只想处理非托管资源,因为使用析构函数会带来一些成本,所以您应该只在消耗宝贵的非托管资源的方法上实现它。
-- C++程序员的C#十大陷阱 文章没有深入探讨这个问题,但在C#中使用析构函数会涉及哪些成本呢?
注意:我知道GC和析构函数不是可靠调用的,除此之外还有其他的吗?
4个回答

8
任何包含终结器的对象(我更喜欢这个术语而不是析构函数,以强调与C++析构函数的区别)都会被添加到终结器队列中。这是一个包含需要在删除前调用终结器的对象引用列表。
当该对象进行垃圾回收时,GC会发现它在终结器队列中,并将该引用移动到可达性(f-reachable)队列中。这是finalizer后台线程依次调用每个对象的终结器方法的队列。
一旦对象的终结器被调用,该对象将不再在终结器队列中,因此它只是一个常规托管对象,GC可以删除它。
所有这些意味着,如果一个对象具有终结器,它将在至少一次垃圾回收之前存活。这通常意味着该对象将被移动到下一个堆生成,这涉及实际将内存中的数据从一个堆移动到另一个堆。

我预计我的大部分对象只会在应用程序的生命周期结束时被拉下并释放,这会对它有什么影响吗?垃圾回收器会一直运行直到清除所有东西,不是吗? - Matthew Scharley
当应用程序结束时,垃圾回收器会允许一些时间来运行终结器,但最终它将关闭堆,即使所有对象都没有被终结。如果您需要运行一些清理代码,则应实现IDisposable接口,该接口可以控制对象的生命周期。 - Guffa
1
强调一下,当应用程序关闭时,.NET 仅允许固定的时间运行终结器。(我认为当前这些值大约是每个终结器最多10秒,所有终结器最多30秒。这意味着不能保证所有终结器都能运行) - jalf

6

如果我有任何编程背景,那就是C语言,尽管我在C#方面的熟练程度比两者都高得多。我只是在搜索C#析构函数信息时找到了这篇文章。 - Matthew Scharley
实际上,C# 3语言规范仍将它们称为析构函数,因此即使它们与C++中的析构函数完全不同,但不幸的是它们仍然共享相同的名称。 - Brian Rasmussen
很少的类应该有终结器。实际上,除了极少数例外,只有主要目的围绕终结的类才应该拥有它们。不应该由持有其他东西的类来持有需要终结的非托管资源(责任); 每个资源都应该移动到其主要目的是持有它并确保其清理(确保其责任得到执行)的类中。由于直接或间接地被可终结对象引用的所有内容都必须保留,直到终结器运行,因此可终结的类应尽可能轻量化。 - supercat

3

Guffa和JaredPar已经很好地涵盖了细节,所以我只想在最终器或析构函数上添加一些有点玄学的注释,不幸的是C#语言规范将它们称为析构函数。

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


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


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