0
点赞
收藏
分享

微信扫一扫

CSharp中CLR的托管堆和垃圾回收(使用需要特殊清理的类型)

大明宫 2022-04-29 阅读 70
C#

现在基本上了解了垃圾回收和托管堆的情况,包括垃圾回收器如何回收对象的内存。信号大多数类型有内存就能正常工作。但有的类型除了内存还需要本机资源。

例如,System.IO.FileStream类型需要打开一个文件(本机资源)并保存文件的句柄。然后,类型的Read和Write方法用句柄操作文件。类似的,System.Threading.Mutex类型要打开一个Windows互斥提体内核对象(本机资源)并保存其句柄,并调用Mutex的方法时使用该句柄。

包含本机资源的类型被GC时,GC会回收对象在托管堆中使用的内存。但这样会造成本机资源(GC对它一无所知)的泄漏,这当然是不允许的。所以,CLR提供了称为终结(finalization)的机制,允许对象在被判定为垃圾之后,但在对象内存被回收之前执行一些代码。任何包装了本机资源(文件、网络连接、套接字、互斥体)的类型都支持终结。CLR判定一个对象不可达时,对象将终结它自己,释放它包装的本机资源。之后,GC会从托管堆回收对象。中级积累System.Object定义了受保护的虚方法Finalize。垃圾回收器判定对象时垃圾后,会调用对象的Finalize方法。

Microsoft的C#团队认为Finalize在编程语言中需要特殊语法。因此C#要求在类名前添加~符号来定义Finalize方法

internal sealed class SomeType
{
    //这是一个Finalize方法
    ~SomeType
    {
        //这里的代码会进入Finalize方法
    }
}

编译上述代码,用ILDasm.exe检查得到的程序集,会发现C#编译器实际是在模块的元数据中生成了名为Finalize的protected override方法。查看Finalize的IL,会发现方法主题的代码被放到一个try块中,在finally块中则放入了一个base.Finalize调用。

重要提示:如果熟悉C++,会发现C#Finalize方法的特殊语法非常类似C++析构器。事实上,在C#语言规范的早期版本中,真的是将该方法称为析构器。但Finalize方法的工作方式和C++析构器完全不同,这回使一种语言迁移到另一种语言的开发人员产生混淆。

问题在于,这些开发人员可能错误的以为使用C#析构器语法意味着类型的实例会被确定性析构,就像在C++中那样。但CLR不支持确定性析构,而作为面向CLR的语言,C#也无法提供这种机制。

被视为垃圾的对象在垃圾回收完毕之后才调用Finalize方法,所以这些对象的内存不是马上被回收的,因为Finalize方法可能要执行访问字段的代码。可终结对象在回收时必须存活,造成他被提升到岭一带,使对象活得比正常时间长。这增大了内存耗用,所以应尽可能避免终结。更糟的是,可终结对象被提升时,其字段给引用的所有对象也会被提升,因为它们也必须要存活。所以,要尽量避免为引用类型的字段定义可终结对象。

另外要注意,Finalize方法的执行时间是控制不了的。应用程序请求更多内存时才可能发生GC,而只有GC完成后才运行Finalize。另外,CLR不保证多个Finalize方法的调用顺序。所以,在Finalize方法不要访问定义了Finalize方法的其他类型的对象;那些对象可能已经终结了。但可以安全的访问值类型的实例,或者访问没有定义Finalize方法的引用类型的对象。调用静态方法也要当心,这些方法可能在内存访问已终结的对象,导致静态方法的行为变得无法预测。

CLR用一个特殊的、高优先级的专用线程调用Finalize方法来避免死锁。如果Finalize方法阻塞(例如进入死循环,或等待一个永远不发出信号的对象),该特殊线程就调用不了任何更多的Finalize方法。这是非常坏的情况,应为应用程序永远回收不了可终结对象张勇的内存-只要应用程序运行就会一致泄漏内存。如果Finalize方法抛出未处理的异常,则进程终止,没办法捕捉该异常。

综上所述,Finalize方法问题较多,使用须谨慎,记住它们是为了释放本机资源而设计的。强烈建议不要重写Object的Finalize方法。相反,使用Microsoft在FCL中提供的辅助类。这些辅助类重写了Finalize方法并添加了一些特殊的CLR魔法

举报

相关推荐

JVM中的垃圾回收概念和算法

0 条评论