【C# .Net GC】清除非托管类型(Finalize终结器、dispose模式以及safeHandler)
总结
1、一般要获取一个内核对象的引用,最好用SafeHandle来引用它,这个类可以帮你管理引用计数,而且用它引用内核对象,代码更健壮
2、托管中生成并引用非托管,一但非托管和托管中的引用断开(托管资源被回收),那么这个时候非托管资源还在,那么释放这个问题就有一丢丢困难。
常见的有两种机制来自动释放非托管资源。
-
声明一个构析函数作为一个类的一个成员 ,继承抽象类safeHandle并重写Dispose或者重写object.Finalize
-
在类中实现System.IDisposable.
清理非托管资源
对于应用创建的大多数对象,可以依赖 .NET 垃圾回收器来进行内存管理。 但是,如果创建包含非托管资源的对象,则当你使用完非托管资源后,必须显式释放这些资源。 最常用的非托管资源类型是包装操作系统资源的对象,如文件、窗口、网络连接或数据库连接。 虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但无法了解如何发布并清理这些非托管资源。
如果你的类型使用非托管资源,则应执行以下操作:
-
实现清理模式。 这要求你提供 IDisposable.Dispose 实现以启用非托管资源的确定性释放。 当不再需要此对象(或其使用的资源)时,类型使用者可调用 Dispose。 Dispose 方法立即释放非托管资源。
-
在类型使用者忘记调用 Dispose 的情况下,请提供一种方法来释放非托管资源。 有两种方法可以实现此目的:
-
使用安全句柄包装非托管资源。 这是推荐采用的方法。 安全句柄派生自 System.Runtime.InteropServices.SafeHandle 抽象类,并包含可靠的 Finalize 方法。 在使用安全句柄时,只需实现 IDisposable 接口并在 Dispose 实现中调用安全句柄的 IDisposable.Dispose 方法。 如果未调用安全句柄的 Dispose 方法,则垃圾回收器将自动调用安全句柄的终结器。
- 重写 Object.Finalize 方法。 当类型使用者无法调用 IDisposable.Dispose 以确定性地释放非托管资源时,终止会启用对非托管资源的非确定性释放。 通过重写 Object.Finalize 方法来定义终结器。但是,由于对象终止是一项复杂且易出错的操作,建议你使用安全句柄,而不是提供你自己的终结器。
-
Dispose模式:强制对象清理资源
Dispose模式:实现了IDisposable接口就实现了Dispose模式。可以使用using语句简化编码。
dispose意思是清理、处理。在完成资源清理后,对象调用dispose()方法后 该对象所占用的内存还暂时不会释放,等着在垃圾回收之后 该对象占用的托管堆内存才被释放 。
Finalize方法确保本地资源的清理,但它的问题是调用时间不确定。另外,由于它不是公共方法,类的用户不能显式调用它。Dispose模式提供了显示进行资源清理的能力。
注意:Dispose只是为了能在确定的时间强迫对象执行清理;并不能控制托管堆中对象所占用内存的生存期。这意味着,即使对象已完成清理,仍可以在它上面调用方法,只不过会抛出System.ObjectDisposedException
终结器方法:强制对象清理资源
要解释这个类型的作用,我们先来了解一下终结方法是干啥的,为什么要定义一个终结方法?
我们定义一个终结方法一般是这个类型“消费”了一些本地资源,这些资源是非托管的,也就是CLR管不了。那类型的开发者就必须显式的销毁这些资源,那我们就给这个类型定义一个终结方法(至于是否该定义终结方法,终结方法到底该怎么用,不在本文讨论之列)。
1、当GC确定一个类型是垃圾的时候,就会着手清理这个类型,但是又发现这个类型实现了一个终结方法,GC就会把这些对象放到一个可终结对象的队列里面,然后一一调用这些对象的终结方法,
2、在调用这些终结方法之前,JIT肯定先要编译这个终结方法(在构造器之前初始化)。System.Object 定义了Finalize方法,但是CLR知道忽略它。类型必须重写Oject的Finalize方法的类型以及派生类型才被认为是可终结的。
3、在GC已经完成栈根不可达对象的标记,GC开始标记终结队列中根指向的可达对象。GC扫描终结列表以查找对这些对象的引用。找到一个引用后,该引用会从终结队列中移除,并附加到F-reachable队列。F-reachable队列是GC内部一种数据结构。F-reachable队列中的引用都代表其Finalize方法以及准备耗调用一个对象。这个过程被称为是对象的复生。
4、在GC压缩后将复活的对象提升到较老的一代,一个特殊的高优先级CLR线程,执行每个对象的Finale方法,最后清空Freachable队列。下一代垃圾回收时,发现以终结的对象成为真正的垃圾。可终结对象需要执行两次的垃圾回收才能释放它们的占用的内存。
5、在递归调用中,标记F-reachable对象时,将递归标记对象中的引用类型的字段所引用的对象,所有的这些对象必须复活以便在回收过程中存活 。之后,GC才结束对垃圾的标识。在这过程中原本被认为是垃圾的对象复活了,然后GC压缩可回收对象的内存。接着特殊的终结线程清空Freach队列,执行每个Finalize方法。 注意F-reachable可终结对象需要执行两次的垃圾回收才能释放他们占用的内存。
CriticalFinalizerObject抽象类解决了以上问题
一切直接或间接的从CriticalFinalizerObject派生的类型,在创建的时候JIT就会将这些个类型的终结方法先给编译了。这下好了,你要回收内存的时候可能没有资源即时编译终结方法,那你创建的时候总有资源吧。为了证明这点我们用Visual Studio + SOS来一探究竟,先上示例代码:
详细请查看:https://www.cnblogs.com/yuyijq/archive/2009/08/09/1542435.html
设计一个类型时,出于以下几个性能方面的原因,最好是避免使用Finalize方法。
- 可终结的对象要花更长的时间来分配,因为指向它们的指针必须放到终结列表中(21.7节“终结操作揭秘”会更详细地讨论)。
- 可终结的对象会提升到较老的代,这会增大内存压力,并在垃圾回收器判定对象为垃圾时阻止回收对象的内存。除此之外,该对象直接或间接引用的所有对象也会被提升(21.14节“代”将讨论提升和代的问题)。
- 可终结的对象导致应用程序的运行速度变慢,这是因为每个对象在回收时,必须对它们进行额外的处理。
Finalize方法在垃圾回收结束时调用,有以下5种事件会导致开始垃圾回收。
第0代满第0代满时,垃圾回收会自动开始。该事件是目前导致 Finalize方法被调用的最常见的一种方式,因为随着应用程序代码运行并分配新对象,这个事件会自然而然地发生
代码显式调用System.GC的静态方法Collect代码可以显式请求CLR执行垃圾回收。虽然 Microsoft强烈建议不要这样做,但某些时候还是有必要的。
Windows报告内存不足
CLR内部使用Win32
CreateMemoryResourceNotification和QueryMemoryResourceNotification函数来监视系统的总体内存。如果Windows报告内存不足,CLR将强制执行垃圾回收,尝试释放已经死亡的对象,从而减小进程工作集的大小。
CLR卸载AppDomain一个AppDomain被卸载时,CLR认为该AppDomain中不再存在任何根,因此会对所有代的对象执行垃圾回收。第22章“CLR寄宿和AppDomain"将讨论AppDomain。
CLR关闭一个进程正常终止时(相对于从外部关闭,比如通过任务管理器关闭),CLR就会关闭。在关闭过程中,CLR会认为该进程中不存在任何根,因此会调用托管堆中的所有对象的Finalize方法。注意,CLR此时不会尝试压缩或释放内存,因为整个进程都要终止,将由Windows负责回收进程的所有内存
。
Finalize 方法的执行时间是控制不了的。CLR 不保证多个 Finalize方法的调用顺序
SafeHandle 模式(终结器+dispose模式)
如果封装本机资源的托管代码时,应该从safeHandle特殊的基类派生出一个类,SafeHandle 派生类非常有用,因为它们保证本机资源在垃圾回收时得以释放。SafeHandle 模式(终结器+dispose模式)
在研究自己的SafeHandle类的实现细节之前,先复习基类型的公共接口:
using System; using System.Runtime.ConstrainedExecution; public abstract class SafeHandle : CriticalFinalizerObject, IDisposable { //这是本机资源的句柄 protected IntPtr handle; protected SafeHandle(IntPtr invalidHandleValue, Boolean ownsHandle) { this.handle = invalidHandleValue; //如果ownsHandle为true,那么这个从SafeHandle派生的对象被回收时, //本机资源会被关闭 } protected void SetHandle(IntPtr handle) { this.handle = handle; } //可调用Dispose显式释放资源 //这是IDisposable接口的Dispose方法 public void Dispose() { Dispose(true); } //默认的Dispose实现(如下所示)正是我们希望的。强烈建议不要重写这个方法 protected virtual void Dispose(Boolean disposing) { //这个默认实现忽略disposing参数; //如果资源已经释放,那么返回; //如果ownsHandle为false,那么返回; //设置一个标志来指明该资源已经释放; //调用虚方法ReleaseHandle; //调用GC.SuppressFinalize(this)方法来阻止调用Finalize方法; //如果ReleaseHandle返回true,那么返回; //如果走到这一步,就激活releaseHandleFailed托管调试助手(MDA) } //默认的Finalize实现(如下所示)正是我们希望的。强烈建议不要重写这个方法 ~SafeHandle() { Dispose(false); } //派生类要重写这个方法以实现释放资源的代码 protected abstract Boolean ReleaseHandle(); public void SetHandleAsInvalid() { //设置标志来指出这个资源已经释放 //调用GC.SuppressFinalize(this)方法来阻止调用Finalize方法 } public Boolean IsClosed { //返回指出资源是否释放的一个标志 get; } public abstract Boolean IsInvalid { //派生类要重写这个属性 //如果句柄的值不代表资源(通常意味着句柄为0或-1),实现应返回true get; } //以下三个方法涉及安全和引用计数 public void DangerousAddRef(ref Boolean success) { ... } public IntPtr DangerousGetHandle() { ... } public void DangerousRelease() { ... } }
CriticalleHandle类,该类出了不提供引用技术功能,其他与safehandle都一样。如果自己写程序,建议只在追求性能时候才使用派生自criticalhandle类。
Critical Finalizer
CriticalFinalizerObject
是一个超级简单的抽象类,它的源码如下:
public abstract class CriticalFinalizerObject
{
protected CriticalFinalizerObject(){}
[SuppressMessage("Microsoft.Performance", "CA1821:RemoveEmptyFinalizers", Justification = "Base finalizer method on CriticalFinalizerObject")]
~CriticalFinalizerObject(){}
}
1、CLR 在首次构造任何
CriticalFinalizerObject派生
类型对象时,CLR立即对对继承层次结构中的所有Finalize方法进行JIT编译,在构造函数之前编译。 目的是为了防止内存紧涨时无法编译终结器,造成本机资源泄漏。
2、如果Finalize方法中的代码引用了另一个程序集中的类型,但是CLR定位该程序集失败,那么资源将得不到释放。
3、CLR是在调用了非CriticalFinalizerObject
派生类型的Finalize方法之后,才调用CriticalFinalizerObject
派生类型的Finalize方法。这样、托管资源类就可以在他们的终结器Finalize方法中安全的访问
CriticalFinalizerObject
派生类的对象。例如,Thread类的Finalize方法可以放心的将数据从内存缓冲区Flush到磁盘,它知道此时的磁盘文件还没关闭)。
4、如果AppDomain 被一个宿主应用程序(例如Microsoft SQL Server或者Microsoft ASP.NET)强行中断,CLR将调用CriticalFinalizerObject派生类型的Finalize方法。宿主应用程序不再信任它内部运行的托管代码时,也利用这个功能确保本地资源得以释放。
和safeHandle类似的类型
SafeRegistryHandle
SafeProcessHandle
SafeThreadHandle
SafeTokenHandle
SafeLibraryHandl
SafeWaitHandle 类的实现方式与上述 SafeFileHandle 类相似。之所以要用不同的类来提供相似的实现,唯一的原因就是为了保证类型安全;编译器不允许将一个文件句柄作为实参传给希望获取一个等待句柄的方法,反之亦然。
其实,所有这些类(还有许多没有列出)已经和 FCL一道发布了,只是没有公开。它们全都在定义它们的程序集内部使用。Microsoft 之所以不公开,是因为不想完整地测试它们,也不想花时间编写它们的文档。但如果想在自己的工作中使用这些类,建议用一个工具(比如ILDasm.exe 或某个IL反编译工具)提取这些类的代码,并将代码集成到自己项目的源代码中。所有这些类的实现其实很简单,自己从头写也花不了多少时间。