jhh0111

常用链接

统计

最新评论

谈IDisposable模式

今天看了“生鱼片”的.net中IDisposable接口这篇文章,发现作者的理解跟我的理解有相当的差异,在此谈谈我对IDisposable接口及Disposable模式的理解。

要讲清楚这个问题,首先需要了解垃圾收集器(GC)的工作原理,这里我分以下几步来简单描述GC回收垃圾的过程:
1、GC发现一个对象(该对象不可达)已经成为垃圾,需要回收;
2、GC查看该对象(或父对象)是否重写了Finalize方法(忽略继承自object.Finalize方法),如果没有,则直接回收该对象的内存,如果有,则GC将该对象移到它内部维护的一个队列(终结可达队列)中,从而使该对象再次可达(称为“复苏”,注意该队列的名字“终结可达”)。
3、当该终结可达队列非空时,GC会唤醒一个线程,该线程对该队列的每个对象执行Finalize方法,并将该对象从队列中移除,从而使该对象再次死亡。
4、再次运行垃圾收集的GC发现该对象彻底死亡,回收该对象的内存。

因此,根据以上的分析,如果该对象包含非托管资源,但正确实现了Finalize方法,那么,GC就能正确释放该非托管资源,不存在内存泄露或无法释放非托管资源的问题。

那么,既然如此,为什么还要提供IDisposable接口呢?原因是,虽然Finalize方法能正确释放非托管资源,但是Finalize方法的调用是非确定的(该方法不能被用户调用),而很多时候,确定性的释放资源,及早的释放资源是很有用的,因此IDisposable接口的作用是为了给程序员提供确定性的释放资源的能力(用户可以确定的调用Dispose方法)。

所以,“生鱼片”提出的以下的写法,我认为是错误的,因为Dispose方法是给用户调用的,而不是给Finalize方法调用的。
public class CaryClass: IDisposable
{
~CaryClass()
{
Dispose();
}
public void Dispose()
{
// 清理资源
}
}
 
接下来,我们来分析一下Dispose模式,代码如下:
private bool IsDisposed=false
public void Dispose() 

     Dispose(true); 
     GC.SupressFinalize(this); 

protected void Dispose(bool Diposing) 

     if(!IsDisposed) 
     { 
         if(Disposing) 
         { 
            //代码1
         } 
         //代码2
     } 
     IsDisposed=true

~CaryClass() 

     Dispose(false); 
}

我们发现,Dispose方法与Finalize方法调用的是同一个方法(带一个bool参数的Dispose方法),这是容易理解的,因为它们的工作是一样的,都是释放资源。但,我们发现,Dispose方法比Finalize方法多执行了“代码1”这部分内容,这是为什么呢?为什么Finalize方法不能执行“代码1”这部分代码呢?

我们先来看一看的IDE自动生成继承自Form的窗口类的代码里的Dispose(bool disposing)方法
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }
我们发现,这里对应的“代码1”这部分的代码为components.Dispose(),而components这个对象是Form窗体类的一个字段,也就是该Form窗口的一个实现了IDispose接口的一个成员。我们知道,如果该窗体要被销毁,那么该窗体的成员也应该被销毁,这是不言而喻的,因此,调用components.Dispose()方法是理所应当的。那同样是销毁窗体,为什么Finalize方法就不调用components.Dispose()方法呢?是不需要,还是不能?

答案是不能。这里还得从Dispose与Finalize这两个方法针对不同的调用者来说起,我们要记住,Dispose是针对用户来调用的,而Finalize是给GC来调用的,正是这个差异导致了Finalize方法不能调用components.Dispose()方法。

我们设想一下这样一种情况,如果Finalize方法也调用components.Dispose()方法,那么当GC首次发现该Form窗体不可达时,GC同时也会发现components不可达(被不可达的对象可达的不算可达),因此,GC就会将这两个对象都移到终结可达队列,然后,GC的一个线程调用该队列中的对象的Finalize方法,该调用是无序的,即GC有可能先调用Form的Finalize方法,再调用components的Finalize方法,也可能先调用components的Finalize方法,再调用Form的Finalize方法。如果发生前一种情况,那么components就会被终结两次,而如果发生后一种情况,那么在调用Form的Finalize方法里的components.Dispose方法时,发现该对象已经被终结,已经不存在了。

而用户调用Dispose方法则不会发生这种情况,因为,调用该方法时,则Form对象一定存在,当然,Form的components也一定存在。

总结:
    1、正确实现Finalize方法就能够保证非托管资源得到正确释放,内存泄露不会发生。
    2、实现IDisposable的唯一理由就是为了确定性的控制资源的释放,弥补Finalize不能确定性的被调用的不足。
    3、实现Dispose模式时,一定要注意,Dispose方法中需要调用各个成员的Dispose方法,Finalize方法不能调用各个成员的Dispose方法。


posted on 2008-06-16 21:46 中华小鹰 阅读(2163) 评论(12)  编辑 收藏 网摘

评论

#1楼  2008-06-16 22:10 ddddl [未注册用户]

这个家伙太NB了.
hi.baidu.com/wangdei1
hi.baidu.com/wangdei2

wangdei.blog.sohu.com/ wangdei@sohu.com
http://5a520.blog.sohu.com/ wangdei163@sohu.com

blog.sina.com.cn/wangdei wangdei1982
blog.sina.com.cn/bt520bt285 wangdeiwangdei
wangdei163.blog.163.com/ wangdei163
wangdei164.blog.163.com/  wangdei164

wangdei.blogcn.com/index.shtml wangdei
i.mop.com/wangdei/blog wangdei
blog.myspace.cn/1306947992 wangdei
wangdei.blogbus.com/ wangdei
blog.tianya.cn/blogger/view_blog.asp?BlogName=wangdei wangdei163   回复  引用    

#2楼  2008-06-16 22:24 wingoo      

最近遇到了内存方面的问题
操作解析xml,然后存取数据到数据库,用下面的两种方式,内存都会不停的增大
在其中gc.collect也没有用处...
1.
foreach()
{
Product p=new Product();
p.title="ddd";
p.desc="ttt";
Dosomething(p);
//p = null;
}

2.
Product p=new Product();
foreach()
{
p.title="ddd";
p.desc="ttt";
Dosomething(p);
}

我想知道最后赋值p=null能不能让gc收回内存?第二种方式p.title重新赋值后,以前的string会不会及时回收?谢谢:)

  回复  引用  查看    

#3楼  2008-06-16 22:53 生鱼片      

o(∩_∩)o...,我说下我的理解:
1.public class CaryClass: IDisposable
{
~CaryClass()
{
Dispose();
}
public void Dispose()
{
// 清理资源
}
}

这种写法只是为了说明将所有清理代码保留在一个位置,以避免代码的重复。

2.托管堆分配的任何内存垃圾收集器都可以确保将其清理,但垃圾收集器却不知道托管堆之外分配的内存如何清理,比如垃圾收集器不知道如何关闭一个文件句柄,以及如何使用 API(如 CoAllocTaskMem)来释放在托管堆之外分配的内存。所以才需要重写 System.Object 的 Finalize 方法(提供析构函数)来实现,使垃圾收集器知道该对象希望参与其自身清理,这样只是让Object的Finalize方法来调用Dispose,但是这个Finalize的运行是不确定的,所以才有了IDisposable模式来手动调用。这样可以尽量保证当对象不可用时尽快释放资源。

3.由于析构函数不再承担对象成员的内存释放,所以你下边说的Finalize方法就不能调用components.Dispose()方法以及顺序问题我也是这样理解的,其他的好像都差不多吧。   回复  引用  查看    

#4楼  2008-06-16 23:16 Gray Zhang      

p=null可以让gc进行收集,但如果之前将p加到了list之类的地方的话就没法收集了
对于string有特殊的intern机制,所以估计不会立刻回收,但也能保证不占用大量内存   回复  引用  查看    

#5楼  2008-06-16 23:36 曲滨*銘龘鶽      

这是官方的原理吗?

不过正确的 IDisposable 写法 msdn 已经给出,和博主的类似   回复  引用  查看    

#6楼  2008-06-16 23:53 Cat Chen      

其实Framework Design Guideline里面说得很清楚了。   回复  引用  查看    

#7楼  2008-06-17 01:54 编织套管 [未注册用户]

浅谈什么是䅄�模式。   回复  引用    

#8楼  2008-06-17 09:57 TONY.chen [未注册用户]

1、GC发现一个对象(该对象不可达)已经成为垃圾

"对象不可达" 这个如何理解??   回复  引用    

#9楼  2008-06-17 10:04 Anytao      

@TONY.chen
对象不可达,简言之就是该对象不被其他对象所引用。GC进行垃圾回收,会根据根对象进行遍历,从而找出所有的可达对象和不可达对象,然后对其中的不可达对象,也就是垃圾对象进行内存清理。这种算法一般称为“标记-收集-紧缩”算法:-)   回复  引用  查看    

#10楼  2008-06-17 10:29 guest [未注册用户]

学习了   回复  引用    

#11楼  2008-06-17 11:02 Ivony [未注册用户]

说的不错,不过有几个小问题:

1、GC查看该对象是否实现Finalize方法(忽略继承自object.Finalize方法)

GC是检查该对象是否有重写Finalize方法,不论是该对象类型重写还是该对象父类型重写,而绝对不是该对象是否实现,因为Finalize方法并不是抽象的。



2、正确实现Finalize方法就能够保证托管资源得到正确释放,内存泄露不会发生。

必须实现Finalize方法如果在你的类型中分配了“非”托管资源,否则必然会造成非托管资源不能被释放,因为GC管不到非托管资源。

IDisposable作为一种友好的接口,在分配非托管资源的类型中是可选实现的。



3、但,我们发现,Dispose方法比Finalize方法多执行了“清理托管资源”这部分内容,这是为什么呢?为什么Finalize方法不能执行“清理托管资源”这部分代码呢?

Dispose方法不是为托管资源准备的,托管资源的释放只能由GC来执行。Dispose方法是提供一种途径提前释放非托管资源,正因为此,即使Dispose方法调用其他对象的Dispose方法也是释放其他对象的“非”托管资源而不是托管资源。

准确的说是Finalize不能使用托管资源,譬如说托管堆上的对象。   回复  引用    

#12楼  2008-06-17 13:30 rIPPER [未注册用户]

msdn上这个问题已经说了很清楚了吧。。。   回复  引用    


标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-07-01 15:31 编辑过
"五向定位"职业成长路线公开课(上海、南京、大连)
Google站内搜索


相关链接: