简介:
这篇文章假定你熟悉我上两篇文章中(GC101, GC102)提到的”Dispose/Finalize”模式。
微软引入了析构模式(pattern of finalization),目的是想使编码更安全。如果一个开发者引用了一个对象(Component)的实例而忘记销毁它(通过调用Dispose方法),此组件仍然能被GC自动回收。
让我来讲解一些实现析构函数的负面效应(译注:即对性能产生负面影响的因素):(对那些没有实现析构函数的对象来说,以下是不存在的):
1. 对象被放入“析构队列”(finalization queue)时
2. 对象从”析构队列”中移走时
3. 对象被置入”将要被析构队列”(to be finalized queue)中时
4. 组件的析构方法要被调用时
5. 对象从“将要被析构的队列”中移走时
6. 你无法准确知道GC要调用析构方法(进行资源回收)的时间
这几点概述了关于使用析构方法时的影响。正如你所见,析构方法对性能有着巨大的冲击。以下让我们继续深度探讨这个问题:
一个析构线程
以上几点中对性能影响最大的就是第4点。.NET的垃圾回收机制有一个专门从“to be finalized queue”调用其析构函数的工作线程。这个线程以高优先级运行,因此如果有很多组件需要释放资源(析构),这个工作线程就会阻塞它们以低优先级执行。如果有很多垃圾要回收,你的进程就会受到拒绝服务攻击(DOS)!
入队/出队
以上提到入队/出队会对性能造成冲击。对简单的对象来说,这是可以接受的。但如果想精确控制它对性能造成的影响,那么就要手工清除了。
你无法控制GC何时执行Finalize()。你只知道它何时应该清理垃圾:当资源不再需要,并且清理垃圾所造成的影响是可以接受的。通过在Dispose()或Close()方法中写入代码,你就能在最洽当的时机进行手工清理资源。
不要引用任何对象
你不能在Finalize()中引用任何已命名的对象。这是因为对象回收是以不可预知的次序执行的,因此你不知道所引用的对象是否之前已被回收掉。这会令你在实现析构时受到某种限制。
降级的垃圾回收(Overall degraded garbage collection)
GC需要经历几个周期才能将析构对象回收。这种影响比它当初看起来的要大得多,不仅会使你的对象存活更久,而且对那些它引用的对象也会如此。
当GC将对象置于”to be finalized queue”中时,它会将GC升级到第1级。第1级中的对象比第0代的对象更少机会被清理,这样你的对象所引用的不再需要的托管/非托管资源就会长时间驻留在内存中。
为什么需要实现析构函数?
是否有什么动机在里面呢?当然啦,这是“确保”清理你的对象占用的资源。这是唯一需要实现析构的原因。如果你的组件使用了资源,它们就要在使用后释放。如果开发者显式地调用了对象的Dispose()方法(假定你在Dispose中调用了GC.SuppressFinalize),那么此资源就会被清理掉,而且你不必再担心它了!如果一个开发者忘记调用对象的Dispose方法,那么GC线程会在调用析构函数时自动清理资源。但是执行析构的时间是随机的,这就不由得你控制了。
结论:
我看见路分两条:
1. 把所有清理资源的代理放在Dispose(或Close)方法中,开发者负责所有的对象清理的工作。如果你的对象使用的是非托管资源,这是合理的,因为如果你忘记清理它们,就会造成内存泄漏。这还会令开发者按需要随时完成清理工作。
2. 实现析构函数,而且只清理非托管资源。在Dispose方法中清理所有的托管资源。
受到最广泛接受的是第2种,但我有些不同,让我来解释为什么:
如果开发者调用对象的Dispose方法失败时,在测量时就会造成内存泄漏(如果没有出,那么测试工作就需要重新修正!)在发现一个泄漏后,可以在此处调用对象Dispose方法。然后就出产生最佳性能。
在一个framework类中,实现析构的方法有很多种。我会在以后发表出来,请留意我的blog。