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


    便于对文章的开展,需要先明确两个概念。

    第一个就是很多人用.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#程序所使用的内存是受托管的,但不意味着滥用,好地编程习惯有利于提高代码的质量以及程序的运行效率。

    http://blog.csdn.net/Knight94/archive/2006/08/05/1023352.aspx

    补充:.NET的GC机制有这样两个问题:

    首先,GC并不是能释放所有的资源。它不能自动释放非托管资源。

    第二,GC并不是实时性的,这将会造成系统性能上的瓶颈和不确定性。

    为了解决第一个问题,.NET提供了析构函数,在C#中是~ClassName的形式。如果某个类定义了析构函数,.NET会在第一次的GC中调用析构函数,第二次才真正进行资源释放。这就允许了我们能够做一些手动的资源管理操作,手动对非托管资源进行清理。但是如果没有必要,定义析构函数就会对性能造成较大的影响。

    仅仅依赖析构函数对非托管资源进行释放是不够的,这是由于第二个问题:GC并不是实时性的,这会造成系统性能上的瓶颈和不确定性。所以有了IDisposable接口,IDisposable接口定义了Dispose方法,这个方法用来供程序员显式调用以释放非托管资源。

    通常我们应该这样写程序:

    代码
    public class SampleClass : System.IDisposable
            {
                    
    public void Dispose()

                    
    //供程序员显式调用的Dispose方法

                    {

                            Dispose(
    true);

                            
    //调用带参数的Dispose方法,释放托管和非托管资源

                            System.GC.SuppressFinalize(
    this);

                            
    //手动调用了Dispose释放资源,那么析构函数就是不必要的了,这里阻止GC调用析构函数

                    }

                    
    protected void Dispose(bool disposing) 

                    
    //protected的Dispose方法,保证不会被外部调用。

                    
    //传入bool值disposing以确定是否释放托管资源

                    {

                            
    if (disposing)

                            {

                                    
    //在这里加入清理"托管资源"的代码,应该是xxx.Dispose();

                            }

                            
    // 在这里加入清理"非托管资源"的代码

                    }

                    
    ~SampleClass()

                    
    //供GC调用的析构函数

                    {

                            Dispose(
    false);

                            
    //释放非托管资源

                    }

            }

            

            这样一来,我们就像Delphi里调用Object.Free方法一样自然的调用Object.Dispose方法,而即使我们忘记了在合适的时候调用Dispose,GC也会在释放对象的时候帮我们清理非托管资源的。GC所充当的角色只是一种保障手段,它应该充当这种角色,我们不能过分依赖它。

            实际上,在较大的模块退出时我们还应该及时地手动调用GC.Collect进行垃圾回收。

  • 相关阅读:
    Qt 之渲染背景浮窗
    idea破解方法
    随机生成数字、随机密码
    Mac上好用的类似xshell的打包软件 Royal TSX
    微信小程序 生成跳转体验版url,可直接跳转到体验版小程序(可通过此方法测试模板消息)
    SpringBoot学习笔记(持续更新)
    Java Web之Tomcat和Servlet
    Java Web之request和response对象
    使用MicrosoftWindowsRPC监控横向渗透
    远程注入BOF
  • 原文地址:https://www.cnblogs.com/jes_shaw/p/1952806.html
Copyright © 2020-2023  润新知