浅谈C#内存回收与Finalize, Dispose, Close方法(二)

来源:互联网 发布:邢帅网络教育 编辑:程序博客网 时间:2024/06/05 05:07

话接上一篇,.NET Framework 提供Object.Finalize方法,它允许对象在垃圾回收器回收该对象使用的内存时适当清理其非托管资源。 默认情况下,Finalize 方法不执行任何操作。 如果您要让垃圾回收器在回收对象的内存之前对对象执行清理操作,您必须在类中重写Finalize 方法。

但是,请注意:若要在 C# 中实现 Finalize 方法,您必须间接使用析构函数语法也就是C#的 ~ClassName() 方法(同C++中的析构语法). 析构函数是在类名前加~.也没有返回值. 不过C#的析构函数的调用机制和C++不同.并不能保证每次都会调用.何时析构由系统作出判断,执行垃圾回收。

c#没有virtual 析构函数,垃圾回收器会正确按次序调用子类和基类的析构函数。

如果我不用析构函数语法呢? 如果你直接override Finalize方法,将产生:

warning CS0465: Introducing a'Finalize' method can interfere with destructor invocation. Did you intend todeclare a destructor?

如果你同事定义Finalize()和析构函数,将会得到error提示:

Error  2      Type'****' already defines a member called 'Finalize' with the same parameter types

 

总之,当一个对象存在Finalize方法时﹐垃圾收集器在收回它的内存之前就会自动调用这个方法。这样我们就可以把那些东东(非托管资源)给清理干净了。

由此看来﹐垃圾收集器提供的这种机制就是为了更好的完善.net的自动内存管理的功能﹐让我们也可以参与到垃圾收集中去。

Note:

1)Finalize 方法不应引发异常,因为应用程序无法处理这些异常,而且这些异常会导致应用程序终止。

2)实现 Finalize 方法或析构函数对性能可能会有负面影响,因此应避免不必要地使用它们。 用 Finalize 方法回收对象使用的内存需要至少两次垃圾回收。 当垃圾回收器执行回收时,它只回收没有终结器的不可访问对象的内存。 这时,它不能回收具有终结器的不可访问对象。 它改为将这些对象的项从终止队列中移除并将它们放置在标为准备终止的对象列表中。 该列表中的项指向托管堆中准备被调用其终止代码的对象。 垃圾回收器为此列表中的对象调用 Finalize方法,然后,将这些项从列表中移除。 后来的垃圾回收将确定终止的对象确实是垃圾,因为标为准备终止对象的列表中的项不再指向它们。 在后来的垃圾回收中,实际上回收了对象的内存。

接着上篇的例子,我们再来看看GC.Collect()这行代码或CLR关闭时.Net做了什么﹕

a.垃圾收集器启动﹐发现fs引用的那个对象已经没用了(当然CLR关闭时才不管你有没有用﹐通通回收)﹐于是对它进行内存回收
b.发现fs的类型﹕FileStream提供了Finalize方法﹐于是先调用这个方法
(以下通过Reflector继续)
c.Finalize方法中有 this._handle.Dispose(...)代码﹐于是调用SafeHandler.Dispose(...)
d.接着转到(当然好多个圈﹐您悠着点...)SafeFileHandle.ReleaseHandle方法﹐发现代码﹕Win32Native.CloseHandle() (即关闭非托管资源--文件HANDLE)

真相大白﹕原来是垃圾收集器帮我们关闭了那个非托管资源(当然还是通过我们自己写的Finalize方法)﹐因此后面就可以删除文件了。

注意:这里的Finalize方法在c#中都是指析构函数。

有人会问﹕好像我们平时在使用FileStream对象时﹐没这么复杂呀?
答﹕Very Good!

一部分人﹕是因为大家都和我的例1一样有好运气﹐那个C盘下的test.txt文件自从被创建后﹐我压根就不会再去用它﹐管它这部分资源有没有被泄漏﹐有没有被锁定﹐最后程序结束时﹐被垃圾收集器帮了忙﹐把忘了关闭的文件HANDLE给收回来了。

剩下的一部分人﹕在程序里埋下了一颗"哑弹"﹐不知什么时候会爆炸﹐就像我例子中的File.Delete方法就出现了异常。

(不过我觉得)绝大多数人﹕是在看了很多诸如.net编程忠告﹐Microsoft强烈建议﹐MSDN标准做法等等等等之后﹐知道了在使用如FileStream,SqlConnection这些东东时﹐必须将其Close。

如何正确使用包装有非托管资源的类?

正确做法应该在使用完那个FileStream后﹐调用fs.Close()将其关闭﹐以保证资源的安全。

认真查看.net类库中的那些基本类别﹐凡是有Finalize方法的类别﹐基本上都提供了诸如Dispose,Close,Dispose(bool)等方法(FileStream也不例外)
区别﹕
Finalize方法﹕只能由微软调用
Dispose和Close方法﹕提供给您调用
因此在您使用完那些类别后﹐那就直接调用Close吧(没有Close﹐再调用Dispose方法)﹐当然万一您忘了﹐也别担心﹐还有垃圾收集器帮您垫后。


如何包装含有非托管资源的类?--Dispose模式

  • 实现带一个Boolean参数的受保护的Dispose方法。该方法释放类封装的所有非托管资源。如果传递给受保护的Dispose的参数是true,那么也对所有包装非托管资源类成员(字段)调用Close或Dispose(从IDisposable中继承的公共Dispose)。
  • 实现.NET框架的IDisposable接口,它包含一个不带参数的Dispose方法。通过调用GC.SuppressFinalize实现公共Dispose,可以防止垃圾回收器调用Finalize,然后调用受保护的Dispose并向它传递true。
  • 重写Finalize。对象被销毁时,垃圾回收器调用Finalize。在Finalize中,调用受保护的Dispose并传递false。这个false很重要,因为它可以防止受保护的Dispose对任何封装类成员(如果垃圾回收器已经运行,那么其中许多成员已经被销毁)调用Close或公共Dispose。
  • 如果语法上是合理的(如类封装的资源可以以文件句柄的形式关闭),那么可以实现调用公共Dispose的Close方法。

class File : IDisposable{    protected IntPtr Handle = IntPtr.Zero;    public File(string name)    {        //TODO: 打开文件并将句柄复制到Handle    }    ~File()    {        Dispose(false);        //"false" means 丢给GC处理时,只管自己的,不要管成员类型中的。成员中的GC也会帮你handle        //各回各家,各找各妈(Finalize方法)    }    public void Dispose()    {        GC.SuppressFinalize(this);        Dispose(true);        //"true" means 抛弃GC,全部自己处理释放,包括成员类型中的非托管资源        //要负责你就负责到底    }    protected virtual void Dispose(bool disposing)    {        //TODO:关闭句柄文件        ///        /// This section only hanle 本类直接包含的非托管资源        ///        if (disposing)        {            //TODO:如果类中内聚的有包装了托管资源的成员                        ///            /// This section handle Data Memeber 的托管资源管理            /// 通过调用Close或Dispose的方法,不可直接操纵            ///        }    }    public void Close()    {        Dispose();    }}


第二个结论﹕

1.在您开发一个封装非托管资源(即类中的字段引用到了非托管资源)的类别时﹕
A:强烈建议您提供Finalize方法进行非托管资源的释放﹐.net垃圾收集器不会帮您自动回收那部分资源﹐而是通过调用您的Finalize方法来帮您释放。(这样可以保证﹕在使用您类别的那位程序员忘了手动回收内存时﹐还可通过垃圾收集器来补救)
B.强烈建议您提供一个Close或Dispose方法﹐以便使用您类别的程序员可以手动释放您的类别中的非托管资源。(参见.net框架程序设计 自动内存管理一章实现Dispose模式)
C.如果类别封装了像FileStream这样的对象(即对非托管资源的再次封装)时﹐一般也应该提供一 个Close或Dispose方法﹐除非您的这个成员保证在每次使用后﹐都被正常的关闭﹐即对调用者透明。
2.在您使用一个封装非托管资源的类别时﹕
A:强烈建议您在明确知道这个类别没有用之后﹐调用其提供的Close或Dispose方法手动释放其非托管资源的 内存。有道是﹕有借有还﹐再借不难;借了不还﹐再借休想~~
B:注意在手动释放后﹐不要再调用该对象的相关方法了﹐因为对象已经损毁了
再次BTW:不管是Finalize﹐Close还是Dispose﹐您都无法显式释放托管堆内存﹐它们永远是微软的"私人财产 "﹕)
 
有人议论:
.net 不要把对象 = null 的;
一位在一般情况下.net的的一个变量如
FileStream fs = newFileStream(@"C:\test.txt", FileMode.OpenOrCreate);
这个 fs 类似c 语言里的指针,只是一个地址而已
= null 是没啥用的
如果等于null 反倒影响gc 回收了


还有如在cpu占有率比较高的情况下 GC 也许回收对象很慢,要比正常情况下慢很多。
原则上,类似FileStream 的对象,使用完后能Close则Close,没有Close方法,﹐再调用Dispose方法。


数据连接对象也是推荐使用 using 代码块自动释放以防止中途出现异常

0 0
原创粉丝点击