您的位置:首页 > 编程语言 > C#

浅谈C#托管程序中的资源释放问题

2010-08-25 09:16 363 查看
(文章来源  曾洪宁ID:Knight94 )http://blog.csdn.net/Knight94/archive/2006/08/05/1023352.aspx
便于对文章的开展,需要先明确两个概念。
第一个就是很多人用 .Net 写程序,会谈到托管这个概念。那么 .Net 所指的资源托管到底是什么意思,是相对于所有资源,还是只限于某一方面资源?很多人对此不是很了解,其实 .Net 所指的托管只是针对内存这一个方面,并不是对于所有的资源;因此对于 Stream ,数据库的连接, GDI+ 的相关对象,还有 Com 对象等等,这些资源并不是受到 .Net 管理而统称为非托管资源。而对于内存的释放和回收,系统提供了 GC-Garbage Collector ,而至于其他资源则需要手动进行释放。
 
那么第二个概念就是什么是垃圾,通过我以前的文章,会了解到 .Net 类型分为两大类,一个就是值类型,另一个就是引用类型。前者是分配在栈上,并不需要 GC 回收;后者是分配在堆上,因此它的内存释放和回收需要通过 GC 来完成。 GC 的全称为“ Garbage Collector ” , 顾名思义就是垃圾回收器,那么只有被称为垃圾的对象才能被 GC 回收。也就是说,一个引用类型对象所占用的内存需要被 GC 回收,需要先成为垃圾。那么 .Net 如何判定一个引用类型对象是垃圾呢, .Net 的判断很简单,只要判定此对象或者其包含的子对象没有任何引用是有效的,那么系统就认为它是垃圾。
 
明确了这两个基本概念,接下来说说 GC 的运作方式以及其的功能。内存的释放和回收需要伴随着程序的运行,因此系统为 GC 安排了独立的线程。那么 GC 的工作大致是,查询内存中对象是否成为垃圾,然后对垃圾进行释放和回收。那么对于 GC 对于内存回收采取了一定的优先算法进行轮循回收内存资源。其次,对于内存中的垃圾分为两种,一种是需要调用对象的析构函数,另一种是不需要调用的。 GC 对于前者的回收需要通过两步完成,第一步是调用对象的析构函数,第二步是回收内存,但是要注意这两步不是在 GC 一次轮循完成,即需要两次轮循;相对于后者,则只是回收内存而已。
 
很明显得知,对于某个具体的资源,无法确切知道,对象析构函数什么时候被调用,以及 GC 什么时候会去释放和回收它所占用的内存。那么对于从 C 、 C++ 之类语言转换过来的程序员来说,这里需要转变观念。
 
那么对于程序资源来说,我们应该做些什么,以及如何去做,才能使程序效率最高,同时占用资源能尽快的释放。前面也说了,资源分为两种,托管的内存资源,这是不需要我们操心的,系统已经为我们进行管理了;那么对于非托管的资源,这里再重申一下,就是 Stream ,数据库的连接, GDI+ 的相关对象,还有 Com 对象等等这些资源,需要我们手动去释放。
 
如何去释放,应该把这些操作放到哪里比较好呢。 .Net 提供了三种方法,也是最常见的三种,大致如下:
1.  析构函数;
2.  继承 IDisposable 接口,实现 Dispose 方法;
3.  提供 Close 方法。
 
经过前面的介绍,可以知道析构函数只能被 GC 来调用的,那么无法确定它什么时候被调用,因此用它作为资源的释放并不是很合理,因为资源释放不及时;但是为了防止资源泄漏,毕竟它会被 GC 调用,因此析构函数可以作为一个补救方法。而 Close 与 Dispose 这两种方法的区别在于,调用完了对象的 Close 方法后,此对象有可能被重新进行使用;而 Dispose 方法来说,此对象所占有的资源需要被标记为无用了,也就是此对象被销毁了,不能再被使用。 例如,常见 SqlConnection 这个类,当调用完 Close 方法后,可以通过 Open 重新打开数据库连接,当彻底不用这个对象了就可以调用 Dispose 方法来标记此对象无用,等待 GC 回收。明白了这两种方法的意思后,大家在往自己的类中添加的接口时候,不要歪曲了这两者意思。
 
接下来说说这三个函数的调用时机,我用几个试验结果来进行说明,可能会使大家的印象更深。
首先是这三种方法的实现,大致如下:
    /// <summary>
    /// The class to show three disposal function
    /// </summary>
    public class DisposeClass:IDisposable
    {
        public void Close()
        {
            Debug.WriteLine( "Close called!" );
        }
 
        ~DisposeClass()
        {
            Debug.WriteLine( "Destructor called!" );
        }
 
        #region IDisposable Members
 
        public void Dispose()
        {
            // TODO:  Add DisposeClass.Dispose implementation
            Debug.WriteLine( "Dispose called!" );
        }
 
        #endregion
    }
 
对于 Close 来说不属于真正意义上的释放,除了注意它需要显示被调用外,我在此对它不多说了。而对于析构函数而言,不是在对象离开作用域后立刻被执行,只有在关闭进程或者调用 GC.Collect 方法的时候才被调用,参看如下的代码运行结果。
        private void Create()
        {
            DisposeClass myClass = new DisposeClass();
        }
 
        private void CallGC()
        {
            GC.Collect();
        }
 
        // Show destructor
        Create();
        Debug.WriteLine( "After created!" );
        CallGC();
 
运行的结果为:
After created!
Destructor called!
 
显然在出了 Create 函数外, myClass 对象的析构函数没有被立刻调用,而是等显示调用 GC.Collect 才被调用。
 
对于 Dispose 来说,也需要显示的调用,但是对于继承了 IDisposable 的类型对象可以使用 using 这个关键字,这样对象的 Dispose 方法在出了 using 范围后会被自动调用。例如:
    using ( DisposeClass myClass = new DisposeClass() )
    {
        //other operation here
    }
 
如上运行的结果如下:
Dispose called!
 
那么对于如上 DisposeClass 类型的 Dispose 实现来说,事实上 GC 还需要调用对象的析构函数,按照前面的 GC 流程来说, GC 对于需要调用析构函数的对象来说,至少经过两个步骤,即首先调用对象的析构函数,其次回收内存。也就是说,按照上面所写的 Dispose 函数,虽说被执行了,但是 GC 还是需要执行析构函数,那么一个完整的 Dispose 函数,应该通过调用 GC.SuppressFinalize(this ) 来告诉 GC ,让它不用再调用对象的析构函数中。 那么改写后的 DisposeClass 如下:
    /// <summary>
    /// The class to show three disposal function
    /// </summary>
    public class DisposeClass:IDisposable
    {
        public void Close()
        {
            Debug.WriteLine( "Close called!" );
        }
 
        ~DisposeClass()
        {
            Debug.WriteLine( "Destructor called!" );
        }
 
        #region IDisposable Members
 
        public void Dispose()
        {
            // TODO:  Add DisposeClass.Dispose implementation
            Debug.WriteLine( "Dispose called!" );
            GC.SuppressFinalize( this );
        }
 
        #endregion
    }
 
通过如下的代码进行测试。
        private void Run()
        {
            using ( DisposeClass myClass = new DisposeClass() )
            {
                //other operation here
            }
        }
 
        private void CallGC()
        {
            GC.Collect();
        }
 
        // Show destructor
        Run();
        Debug.WriteLine( "After Run!" );
        CallGC();
 
运行的结果如下:
Dispose called!
After Run!
 
显然对象的析构函数没有被调用。通过如上的实验以及文字说明,大家会得到如下的一个对比表格。
 
析构函数
Dispose 方法
Close 方法
意义
销毁对象
销毁对象
关闭对象资源
调用方式
不能被显示调用,会被 GC 调用
需要显示调用
或者通过 using 语句
需要显示调用
调用时机
不确定
确定,在显示调用或者离开 using 程序块
确定,在显示调用时
 
那么在定义一个类型的时候,是否一定要给出这三个函数地实现呢。
 
我的建议大致如下。
1. 提供析构函数,避免资源未被释放,主要是指非内存资源;
2. 对于 Dispose 和 Close 方法来说,需要看所定义的类型所使用的资源(参看前面所说),而决定是否去定义这两个函数;
3. 在实现 Dispose 方法的时候,一定要加上“ GC.SuppressFinalize( this ) ”语句,避免再让 GC 调用对象的析构函数。
 
C# 程序所使用的内存是受托管的,但不意味着滥用,好地编程习惯有利于提高代码的质量以及程序的运行效率。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息