说到内存管理,C\C++的开发人员肯定遇到过各种各样令人头疼的内存管理问题,以及复杂的指针问题。而相比较而言,Java和C#的开发人员就幸福多了,这两种语言都提供了垃圾回收机制,这极大地减轻了开发人员的内存管理工作。垃圾回收由运行时负责,大部分时间开发人员都不需要开关注此类问题。但是,大部分时间并不是所有时间,例如C#当涉及到非托管的资源时,就需要开发人员自己处理。而 IDisposable.Dispose()
和 Object.Finalize()
就是其中重要的两个方法。本文主要介绍下二者的使用方法、区别与联系和最佳实践。
1 引言
.NET中主要有两种资源:托管的资源和非托管的资源。顾名思义,托管的资源由.NET运行时管理,非托管的资源由开发人员管理。
托管资源指的是.NET可以自动进行回收的资源,主要是指托管堆上分配的内存资源。托管资源的回收工作是不需要人工干预的,由.NET运行时在合适的时机调用垃圾回收器进行回收。
非托管资源指的是.NET不知道如何回收的资源,最常见的一类非托管资源是包装操作系统资源的对象,例如文件,窗口,网络连接,数据库连接,画刷,图标等。这类资源,垃圾回收器在清理的时候会调用 Object.Finalize()
方法。默认情况下,方法是空的,对于非托管对象,需要在此方法中编写回收非托管资源的代码,以便垃圾回收器正确回收资源。
但是有些非托管资源往往比较宝贵,而垃圾回收是由.NET运行时执行的。这样就无法保证及时地释放掉这些资源。鉴于此,.NET提供了 IDisposeable
接口。凡是实现了该接口的对象,使用者都可以手动调用该对象的Dispose方法释放资源,以保证相关资源及时回收,提高资源使用效率。
二者均可以进行资源回收,只是使用场景,调用时机和调用对象不同而已。
2 Object.Finalize()简介
Object.Finalize
用于在运行时进行垃圾回收时, 回收非托管的资源 ,这点很重要。 他跟垃圾回收器有着紧密的联系。这里只是简单提下垃圾回收机制,下一篇文章再详细介绍。当一个对象被创建时,垃圾回收器会检查该对象有没有自定义 Finalize()
方法。如果已自定义,它会把这个对象标记为 Finalizable 并且将一个指向该对象的指针存入到 finalization queue
。当该对象需要被回收时,如果 finalization queue
中有指针指向该对象,垃圾回收器会将该对象从堆上拷贝至另外一个托管的结构体中, finalization reachable table
。 重要 在下一次垃圾回收时,垃圾回收器会创建一个单独的线程,调用所有在 finalization reachable table
中的对象的 Finalize()
方法。所以,Finalizeable的对象被完全回收需要 至少两个 垃圾回收周期。以下内容出自 Pro C#5.0 and the .NET Framework :
When you allocate an object onto the managed heap, the runtime automatically determines whether your object supports a custom Finalize() method. If so, the object is marked as finalizable, and a pointer to this object is stored on an internal queue named the finalization queue. The finalization queue is a table maintained by the garbage collector that points to each and every object that must be finalized before it is removed from the heap.
When the garbage collector determines it is time to free an object from memory, it examines each entry on the finalization queue and copies the object off the heap to yet another managed structure termed the finalization reachable table (often abbreviated as freachable, and pronounced “effreachable”). At this point, a separate thread is spawned to invoke the Finalize() method for each object on the freachable table at the next garbage collection. Given this, it will take, at the very least, two garbage collections to truly finalize an object.
但是开发人员不允许直接重载 Finalize()
方法,而是只能重载析构方法。我觉得主要是出于程序鲁棒性方面的考虑。下面是重载析构方法的样例代码。
// Override System.Object.Finalize() via finalizer syntax. |
编译后生成的IL代码:
.method family hidebysig virtual instance void Finalize() cil managed { // Code size 13 (0xd) .maxstack 1 .try { IL_0000: ldc.i4 0x4e20 IL_0005: ldc.i4 0x3e8 IL_000a: call void [mscorlib]System.Console::Beep(int32, int32) IL_000f: nop IL_0010: nop IL_0011: leave.s IL_001b } // end .try finally { IL_0013: ldarg.0 IL_0014: call instance void [mscorlib]System.Object::Finalize() IL_0019: nop IL_001a: endfinally } // end handler IL_001b: nop IL_001c: ret } // end of method MyResourceWrapper::Finalize
由此可以看出,编译后,析构方法的代码会被拷贝至 Finalize()
方法的 try 块中,以防止自定义析构方法抛出异常影响垃圾回收器的正常运行。
3 IDisosable.Dispose简介
接口 IDisposable
是对 Object.Finalize()
方法的补充,使用该接口可以及时地释放资源,提高资源使用效率。其原理也比较简单,垃圾回收不再由垃圾回收器负责,而是由调用方手动触发。当然触发得越及时,资源回收得越及时。假设现在有一个资源类 MyResourceWrapper
实现了该接口:
// Implementing IDisposable. |
我们可以这样使用:
static void Main(string[] args) |
借助于C#的语法糖,我们也可以这样使用:
static void Main(string[] args) |
编译器编译后生成的中间代码是一致的。
4 最佳实践
自定义 Finalize()
方法可以在一定程度上解放开发人员,将资源回收的工作完全交给垃圾回收器,但是资源利用率比较低。 实现 IDisposable
接口可以提高资源的利用率,
但是可能会出现调用者忘记调用的情况,使资源得不到释放。人总有犯错误的时候。所以最佳的实践就是将二者结合起来一起使用,二者相得益彰。微软给出了标准的Dispose Pattern。
在Visual Studio中,如果一个类想要实现IDisposable接口,点击左侧的提示菜单,VS就会自动帮你生成如下代码:
public class MyResourceWrapper:IDisposable |
这里有一个地方不太容易理解,即
if (disposing) |
这个地方为什么只有 IDisposable.Dispose()
方法调用时才生效?其实这跟.NET的垃圾回收机制有关,前面也提到过,如果自定义 Finalize()
方法,该方法的调用需要至少两个垃圾回收周期。
而等到那时候,那些需要释放资源的托管对象是否存在就不得而知了。
5 总结
就释放资源来讲,二者均可以达到目的。但是二者还是有些不同的:
IDisposable.Dispose | Finalize() | |
---|---|---|
调用方 | 使用者 | 垃圾回收器 |
适用场景 | 需要及时释放资源,对象的生命周期确定 | 对资源释放时间无特殊要求,对象生命周期不确定 |
如果对象只在单线程中使用,那么其生命周期是确定的;但是如果多个线程都使用了该对象,那么其生命周期就不太好确定了。不论如何,还是最好使用微软给出的Dispose Pattern, 将二者结合使用。