使用终结器来释放本地资源

前面我们基本了解了垃圾回收和托管堆得情况了,包含垃圾回收期如何回收对象的内存,幸运的是,大多数类型只要内存就可以正常工作,但是,另外有一些类型除了使用内存,还要使用本地资源。

例如:System.IO.FileStream类型需要打开一个文件(本地资源)并保存文件的句柄。然后,该类型的Read和Write方法使用该句柄来操作文件,类似的,System.Threading.Mutex类型打开一个WINDOWS互斥体内核对象(本地资源)并保存其句柄,并在调用Mutex方法时使用该句柄。终结(finalization)是CLR提供的一种机制,允许对象在垃圾回收之前进行一些得体的清理工作。任何包含了本地资源(例如文件、网络连接、套接字、互斥体和其他类型)的类型都必须支持终结器。简单的说,类型实现了一个命名为Finalize的方法。当垃圾回收期判断一个对象时垃圾时,会调用对象的Finalize方法。可以这么理解:实现了Finalize方法的任何类型实际上是在说,它的所有对象都希望在“被处决前吃上最后一餐”。

C#团队认为,Finalize方法是在编程语言中需要特殊语法的一种方法。因此,在C#中,必须在类名之前加一个~符号来定义Finalize方法,如下代码所示:

Public      class      SomeType

{

~SomeType()

{}

}

编译上诉代码,在ILDasm.exe检查得到的程序集,会发现C#编译器实际是在模块的元数据中生成一个名为Finzlize的方法。

查看方法的元数据,会发现在一个try {} finally{}代码块中,finally块中调用了对基类的Base.Finalize()方法。实现Finalize方法时,一般会调用Win 32的 CloseHandle函数,并向其函数传递本地资源的句柄。例如:FileStream类型定义了一个文件句柄字段,它标示了本地资源。FileStream类型还定义了Finalize方法,它在内部调用CloseHandle函数,并向它传递文件句柄字段。这就确保了在托管的FileStream对象被确定为垃圾之后,本地文件句柄会得以关闭。如果包装了本地资源的类型没有定义Finalize方法,本地资源就没法得到关闭,会导致资源泄露,直到进程终止。进程终止时,这些资源才被本地回收。

1.使用CriticalFinalizerObject 类型确保终结

为了简化编程System.Runtime.ConstrainedExecution命名空间中定义了一个CriticalFinalizerObject类

查看一下该类,发现该类没有什么特别之处,但是CLR以一种特殊的方式对待该类以及其派生类。具体的说CLR赋予了这个类以下三个很酷的功能。

1)首先构造任何CriticalFinalizerObject派生类的一个对象时,CLR立即对继承层次中的所有Finalize方法进行JIT编译。在构造对象时编译这些方法,可确保对象被定为垃圾之后,本地资源肯定得以释放。如果不对Finalize方法提前编译,那么也许能确保编译并使用本地资源,但无法保证会肯定释放这些资源。内存紧张时,CLR可能找不到足够的内存来编译Finalize方法,这回阻止Finzlize方法的执行,造成本地资源泄露。另外,如果Finalize方法中的代码引用了另一个程序集中的一个类型,而且CLR在寻找这个程序集时失败,也会造成资源泄露。

2)CLR在调用一个非CriticalFinalizerObject派生类型的Finalize方法之后,才能调用CriticalFinalizerObject派生类型的Finalize方法。这样一来,托管资源类就可以在他们的Finalize方法中成功访问CriticalFinalizerObject派生类的对象。

3)如果一个AppDomain被一个宿主应用程序(例如 Sql server,asp.net)强行中断,CLR会调用CriticalFinalizerObject派生类型的Finalize方法。宿主应用程序不再信任它内部运行的托管堆代码时,也利用这个功能确保本地资源得以释放。

2.SafeHandle类型及派生类型

现在Microsoft意识到最常用的本地资源就是Windows提供的资源。还意识到大多数资源都是用句柄进行操作的,同样的为了简化编程、更安全,System.Runtime.InteropServices命名空间中定义了一个SafeHandle类,其形式如下:

对于SafeHandle类,其关注两点。其一,它派生字CriticalFinalizerObject;这确保会得到CLR的特殊对待。其二,他是一个抽象类,必须有一个类从该类派生,并重写受保护的构造器、抽象方法ReleaseHandle以及抽象属性IsInvalid的get访问器方法。

在Window中,如果句柄的值为0或者-1,那么他们中的大多数都是无效的,命名空间Microsoft.Win32.SafeHandles中包含了一个SafeHandleZeroOrMinusOneIsInvalid的辅助类。

同样的该类也是一个抽象类。必须有另外一个类派生字该类,并重写受保护的构造器和抽象方法ReleaseHandle方法。.NET FrameWork只提供了很少几个从SafeHandleZeroOrMinusOneIsInvalid派生的类,其中包含SafeFileHandle,SafeRegistryHandle,SafeWaitHandle和SafeBuffer。以下是SafeFileHandle类

SafeWaitHandle的实现方法与SafeFileHandle的实现方法一样,内部都是调用Win32Native.CloseHandle();方法相同的代码,实现了不同的类,是因为确保类型安全;编译器不希望将一个文件句柄作为实参传给一个等待句柄的方法。SafeRegistryHandle类的ReleaseHandle方法调用Win 32 regCloseKey函数。

.net framework还提供了一些附加的类来包装本地资源:SafeProcessHandle, SafeThreadHandle,SafeTokenHandle,SafeFileMappingHandle,SafeViewOfFileHandle,SafeLibraryHandle以及SafeLocalAllocHandle.

其实,所有这些类库已经和Framework Class Library(FCL)一道发布。但是这些类没有对外公开,他们全部在MSCorLib.dll和System.dll内部使用,微软之所以没有公开这些类是因为他们不想完整的测试他们,也不想花时间编写他们的文档。如果希望使用这些类,你可以利用反编译工具,将这些类提取出来,放到自己的代码中。

3.使用SafeHandle类型与非托管代码进行交互

如前所诉:SafeHandle派生类非常有用,因为他们能保证在发生垃圾回收的时,本地资源得以释放。除了讨论过的功能,SafeHandle还提供了另两个功能。首先,要和非托管代码进行交互,在这种情况下使用SafeHandle派生类将获得CLR的特殊对待。

在上面的代码中,CreateEventBad方法的原型是返回一个IntPtr,在.NET FRAMEWORK 2.0之前,SafeHandle类是不存在的,所以不得不使用IntPtr类型来标示句柄。CLR团队发现这种代码不够健壮,在调用CreateEventBad之后,在句柄赋值个handle变量之前,可能抛出一个ThreadAbortExcption。虽然这种情况很少发生,但是一旦发生,将造成资源泄露。为了关闭方法创建的事件,唯一的办法是终止进程。

现在,在.NET FrameWork 2.0和以后的版本中,可以使用SafeHandle来修正这个潜在的资源泄露问题,注意,CreateEventGood方法的原型是返回一个SafeWaitHandle,在调用CreateEventGood方法时,CLR调用Win32 CreateEvent函数。CreateEvent返回到托管代码时,CLR知道SafeWaitHandle是从SafeHandle派生得来的。所以会自动构造一个SafeWaitHandle的一个实例,并在构造时传递从CreateEvent返回的句柄值。新的SafeWaitHandle对象的构造以及句柄的赋值是在非托管代码中进行的。不可能被一个ThreadAbortExcption打断。现在托管代码已经不可能泄露这个本地资源了,最后SafeWaitHandle对象被垃圾回收,它的Finalize方法会被调用,确保资源得以释放。

SafeHandle派生类的最后一个功能是防止有人利用潜在的安全漏洞。现在的一个问题是。一个线程可能试图使用一个本地资源,同时另一个线程正在释放该资源。这可能造成一个句柄循环漏洞。SafeHandle类防止这个安全隐患的方法时使用引用计数。在内部,SafeHandle定义一个私有字段类维护一个计数器。一旦某个Safehandle的派生对象被设为一个有效的句柄,计数器就被设置成1,每次将一个SafeHandle派生类对象作为参数传给一个人非托管方法,CLR就会自动递增计数器。类似的,将非托管方法返回托管代码时,CLR会自动递减计数器。当然对计数器的操作是以线程安全的方式去做的,这是怎么促进安全性的呢?当一个线程试图释放SafeHandle对象包装的本地资源时,CLR实际上知道它不能释放该资源,因为该资源正在由一个非托管代码使用。非托管代码返回后,计数器递减为0,资源才能得以释放。

还要注意System.Runtime.InteropServices;的命名空间中还有一个CriticalHandle类,该类不引用计数器功能,其他方面和SafeHandle类相同,该类以及派生类通过牺牲安全性来换取性能。CriticalHandle类也有自己的派生类,

其中有CriticalHandleZeroOrMinusOneIsInvalid ,CriticalHandleMinusOneIsInvalid ,由于微软倾向于构建更安全而不是更快的系统,所以类库中没有提供这两个派生的类。

时间: 2024-10-11 05:44:26

使用终结器来释放本地资源的相关文章

对托管资源使用终结器

重要提示:有的人可能有这样的心态,永远不要对托管资源使用终结器,我在很大程度上赞成这个观点,所以可以完全跳过本节,对托管资源使用终结器,是非常高的编码方式,只有极少数情况下才应该使用,要是使用必须对Finalize方法中的调用的代码有一个全面和深刻的认识.另外,还必须保证调用的代码的行为在未来的版本中不会发生改变.具体的说,Finalize方法中调用的任何代码都不能使用其他任何可能已终结的对象. 虽然终结操作是专门来释放本地资源,但偶尔也用于托管资源,下面这个类造成计算机在垃圾回收器每执行一次回

C#析构函数(destructor)和终结器(Finalizer) .

使用析构函数释放资源 析构函数用于析构类的实例. 1)         不能在结构中定义析构函数.只能对类使用析构函数. 2)         一个类只能有一个析构函数. 3)         无法继承或重载析构函数. 4)         无法调用析构函数.它们是被自动调用的. 5)         析构函数既没有修饰符,也没有参数. 例如,下面是类 Car 的析构函数的声明: [csharp] view plaincopy class Car { /// <summary> /// 析构函

编写高质量代码改善C#程序的157个建议——建议47:即使提供了显式释放方法,也应该在终结器中提供隐式清理

建议47:即使提供了显式释放方法,也应该在终结器中提供隐式清理 在标准的Dispose模式中,我们注意到一个以~开头的方法,如下: /// <summary> /// 必须,防止程序员忘记了显式调用Dispose方法 /// </summary> ~SampleClass() { //必须为false Dispose(false); } 这个方法叫做类型的终结器.提供类型终结器的意义在于,我们不能奢望类型的调用者肯定会主动调用Dispose方法,基于终结器会被垃圾回收这个特点,它被

垃圾回收GC:.Net自动内存管理 上(三)终结器

垃圾回收GC:.Net自动内存管理 上(三)终结器 垃圾回收GC:.Net自动内存管理 上(一)内存分配 垃圾回收GC:.Net自动内存管理 上(二)内存算法 垃圾回收GC:.Net自动内存管理 上(三)终结器 前言 .Net下的GC完全解决了开发者跟踪内存使用以及控制释放内存的窘态.然而,你或午想要理解GC是怎么工作的.此系列文章中将会解释内存资源是怎么被合理分配及管理的,并包含非常详细的内在算法描述.同时,还将讨论GC的内存清理流程及什么时清理,怎么样强制清理. 终结器 GC提供了另外一个能

垃圾回收GC:.Net自己主动内存管理 上(三)终结器

垃圾回收GC:.Net自己主动内存管理 上(三)终结器 垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(二)内存算法 垃圾回收GC:.Net自己主动内存管理 上(三)终结器 前言 .Net下的GC全然攻克了开发人员跟踪内存使用以及控制释放内存的窘态.然而,你或午想要理解GC是怎么工作的.此系列文章中将会解释内存资源是怎么被合理分配及管理的,并包括很具体的内在算法描写叙述.同一时候,还将讨论GC的内存清理流程及什么时清理,怎么样强制清理. 终结

关于在android层释放webrtc资源的问题

最近一段时间在做基于webrtc的android应用在释放资源时遇到一些问题,现在记录下来用于备忘. 官方给出的AppRTCDemo太过于简单很多问题没涉及到. 1.释放peerconnection资源的问题. 场景:A和B进行通话(视频通话) 现在B中终止通话 错误:在B终止通话之后,A端的程序程序会意外退出. 分析:在A和B进行通话的时候会见了相应的PeerConnection类实例,这个实例保存的有stream的引用(localstream和remote stream等等). B在终止通话

ArcGIS Engine中正确释放打开资源

转自原文 ArcGIS Engine中正确释放打开资源 AE中对MDB,SDE等数据库操作时,打开后却往往不能及时释放资源,导致别人操作提示对象被锁定. 很多帖子说了很多原理,看的也烦且不实用,比如一句话概括的用System.Runtime.InteropServices.Marshal.ReleaseComObject(object o)释放,说的很不清楚,很多人试过觉的释放不掉. 事实上,的确是用该方法,但释放的技巧在于,新建几个AE对象就要逐步释放几个,例如: IWorkspaceFact

本地向服务器上传文件的方式-本地资源映射到服务器

本地向服务器传文件一定使不少刚接触服务器端工作的人疑惑,笔者研究生期间参与维护学校科技部的ASP.NET的IIS服务器,服务器上没有安装FTP工具,为了安全考虑网络也没有打开,windows自带的FTP服务也没有打开的情况下,远程连接时使用本地磁盘映射方法很方便实现本地与服务器的交互. 下面介绍两种方式交互(本地主机win10,远程win2003): 本地磁盘映射到服务器方式的步骤: 1 win + R 输入mstsc指令,打开远程桌面,输入ip 2点击显示选项 点击本地资源>详细信息 选择磁盘

第十一节:用于本地资源的其它垃圾回收功能

有时,本地资源会消耗大量的内存,但是用于包装该资源的托管对象只占用了非常少的内存.一个典型的例子就是位图.一个位图可能占用几兆字节的本地内存,但是托管对象却极小,因为它只包含了一个hbitmap(一个4或8字节的值),从CLR角度看,一个进程可以在执行一次垃圾回收之前分配数百个位图(他们用的托管内存太少了).但是,如果进程操作许多位图,进程的内存消耗将以一个恐怖的速度增长.为了修正这个问题,GC类提供了以下两个静态方法: public static void AddMemoryPressure(