• .NET垃圾回收机制


    在.net 编程环境中,系统的资源分为托管资源和非托管资源。 
      对于托管的资源的回收工作,是不需要人工干预回收的,而且你也无法干预他们的回收,所能够做的只是了解.net CLR如何做这些操作。也就是说对于您的应用程序创建的大多数对象,可以依靠 .NET Framework 的垃圾回收器隐式地执行所有必要的内存管理任务。

      对于非托管资源,您在应用程序中使用完这些非托管资源之后,必须显示的释放他们,例如System.IO.StreamReader的一个文件对象,必须显示的调用对象的Close()方法关闭它,否则会占用系统的内存和资源,而且可能会出现意想不到的错误。

      我想说到这里,一定要清楚什么是托管资源,什么是非托管资源了?

      最常见的一类非托管资源就是包装操作系统资源的对象,例如文件,窗口或网络连接,对于这类资源虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但它不了解具体如何清理这些资源。还好.net Framework提供了Finalize()方法,它允许在垃圾回收器回收该类资源时,适当的清理非托管资源。如果在MSDN Library 中搜索Finalize将会发现很多类似的主题,这里列举几种常见的非托管资源:ApplicationContext,Brush,Component,ComponentDesigner,Container,Context,Cursor,FileStream,Font,Icon,Image,Matrix,Object,OdbcDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,Timer,Tooltip 等等资源。可能在使用的时候很多都没有注意到!

    关于托管资源,就不用说了撒,像简单的int,string,float,DateTime等等,.net中超过80%的资源都是托管资源。

    非托管资源如何释放,.NET Framework 提供 Object.Finalize 方法,它允许对象在垃圾回收器回收该对象使用的内存时适当清理其非托管资源。默认情况下,Finalize 方法不执行任何操作。默认情况下,Finalize 方法不执行任何操作。如果您要让垃圾回收器在回收对象的内存之前对对象执行清理操作,您必须在类中重写 Finalize 方法。然而大家都可以发现在实际的编程中根本无法override方法Finalize(),在C#中,可以通过析构函数自动生成 Finalize 方法和对基类的 Finalize 方法的调用。

    例如: 
    ~MyClass()

      // Perform some cleanup operations here. 

      该代码隐式翻译为下面的代码。
    protected override void Finalize() 
    {
      try
      {
        // Perform some cleanup operations here.
      }
      finally
      {
        base.Finalize();
      }
    }

    但是,在编程中,并不建议进行override方法Finalize(),因为,实现 Finalize 方法或析构函数对性能可能会有负面影响。一个简单的理由如下:用 Finalize 方法回收对象使用的内存需要至少两次垃圾回收,当垃圾回收器回收时,它只回收没有终结器(Finalize方法)的不可访问的内存,这时他不能回收具有终结器(Finalize方法)的不可以访问的内存。它改为将这些对象的项从终止队列中移除并将他们放置在标记为“准备终止”的对象列表中,该列表中的项指向托管堆中准备被调用其终止代码的对象,下次垃圾回收器进行回收时,就回收并释放了这些内存。


    C#托管及未托管对象管理

     c#中的对象分为值类型和引用类型,二者最大的区别在于数据的存储方式和存储位置.WINDOWS操作系统使用虚拟寻址系统来管理程序运行时产生的数据存放.简单的说,该系统管理着一个内存区域,在该区域中划拨出一部分出来专门存放值类型变量,称为堆栈,堆栈采用先进后出的原则,将值类型变量从区域的最高地址位开始向低位地址存储,先进后出,后进先出的管理方式保证了值类型变量在出了作用域后能即使的清除占用的内存区域,由于堆栈速度快,所保存的数据一般不太大,这部分一般不需要用户专门操作. 值类型保存在堆栈汇总, 堆栈有非常高的性能,但对于所有的变量来说还是不太灵活。通常我们希望使用一个方法分配内存,来存储一些数据,并在方法退出后的很长一段时间内数据仍是可以使用的。只要是用new运算符来请求存储空间,就存在这种可能性——例如所有的引用类型。此时就要使用托管堆。它在垃圾收集器的控制下工作,托管堆(或简称为堆)是系统管理的大内存区域中的另一个内存区域。要了解堆的工作原理和如何为引用数据类型分配内存,看看下面的代码: 
    Customer arabel = new Customer();
    这行代码完成了以下操作:首先,分配堆上的内存,以存储Customer实例(一个真正的实例,不只是一个地址)。然后把变量arabel的值设置为分配给新Customer对象的内存地址(它还调用合适的Customer()构造函数初始化类实例中的字段,但我们不必担心这部分)。
    Customer实例没有放在堆栈中,而是放在内存的堆中。如果我们这样操作:
    Customer newaddress = arabel ;
    这时候,newaddress也会保存在堆栈中,其值和arabel 相同,都是存储Customer实例的堆地址.
         知道了这些,我们会发现这样一个问题,如果堆栈中arabel 和newaddress两个变量过期销毁,那堆中保存的Customer对象会怎样?实际上它仍保留在堆中,一直到程序停止,或垃圾收集器删除它为止. C#的垃圾收集器如果没有显示调用,会定时运行并检查内存,删除没有任何变量引用的数据.看起来似乎不错,但是想想,垃圾回收器并不是时时检查,它是定时运行,而在这段时间内如果产生大量的过期数据驻留在内存中..... 那么或许我们可以通过调用System.GC.Collect(),强迫垃圾收集器在代码的某个地方运行,System.GC是一个表示垃圾收集器的.NET基类, Collect()方法则调用垃圾收集器。但是,这种方式适用的场合很少,(难道销毁一个对象就让垃圾回收检查一便内存吗?)例如,代码中有大量的对象刚刚停止引用,就适合调用垃圾收集器。况且垃圾收集器的逻辑不能保证在一次垃圾收集过程中,从堆中删除所有过期数据,对于不受垃圾回收器管理的未托管对象(例如文件句柄、网络连接和数据库连接),它是无能为力的。那该怎么做呢?
      这时需要制定专门的规则,确保未托管的资源在回收类的一个实例时释放。
    在定义一个类时,可以使用两种机制来自动释放未托管的资源。这些机制常常放在一起实现,因为每个机制都为问题提供了略为不同的解决方法。这两个机制是:
    ●         声明一个析构函数,作为类的一个成员
    ●         在类中实现System.IDisposable接口
    下面依次讨论这两个机制,然后介绍如何同时实现它们,以获得最佳的效果。
    析构函数
    前面介绍了构造函数可以指定必须在创建类的实例时进行的某些操作,在垃圾收集器删除对象时,也可以调用析构函数。由于执行这个操作,所以析构函数初看起来似乎是放置释放未托管资源、执行一般清理操作的代码的最佳地方。但是,事情并不是如此简单。由于垃圾回收器的运行规则决定了,不能在析构函数中放置需要在某一时刻运行的代码,如果对象占用了宝贵而重要的资源,应尽可能快地释放这些资源,此时就不能等待垃圾收集器来释放了.
    IDisposable接口
    一个推荐替代析构函数的方式是使用System.IDisposable接口。IDisposable接口定义了一个模式(具有语言级的支持),为释放未托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾函数器相关的问题。IDisposable接口声明了一个方法Dispose(),它不带参数,返回void,Myclass的方法Dispose()的执行代码如下:
    class Myclass : IDisposable
    {
        public void Dispose() 
        {
           // implementation
        }
    }
    Dispose()的执行代码显式释放由对象直接使用的所有未托管资源,并在所有实现IDisposable接口的封装对象上调用Dispose()。这样,Dispose()方法在释放未托管资源时提供了精确的控制。
    假定有一个类ResourceGobbler,它使用某些外部资源,且执行IDisposable接口。如果要实例化这个类的实例,使用它,然后释放它,就可以使用下面的代码: 
    ResourceGobbler theInstance = new ResourceGobbler();

        // 这里是theInstance 对象的使用过程
      
    theInstance.Dispose();
    如果在处理过程中出现异常,这段代码就没有释放theInstance使用的资源,所以应使用try块,编写下面的代码:
    ResourceGobbler theInstance = null;
    try
    {
        theInstance = new ResourceGobbler();
    //   这里是theInstance 对象的使用过程
    }
    finally  
    {
       if (theInstance != null) theInstance.Dispose();
    }
    即使在处理过程中出现了异常,这个版本也可以确保总是在theInstance上调用Dispose(),总是释放由theInstance使用的资源。但是,如果总是要重复这样的结构,代码就很容易被混淆。C#提供了一种语法,可以确保在引用超出作用域时,在对象上自动调用Dispose()(但不是Close())。该语法使用了using关键字来完成这一工作—— 但目前,在完全不同的环境下,它与命名空间没有关系。下面的代码生成与try块相对应的IL代码:
    using (ResourceGobbler theInstance = new ResourceGobbler())
    {
        //   这里是theInstance 对象的使用过程
    }
    using语句的后面是一对圆括号,其中是引用变量的声明和实例化,该语句使变量放在随附的复合语句中。另外,在变量超出作用域时,即使出现异常,也会自动调用其Dispose()方法。如果已经使用try块来捕获其他异常,就会比较清晰,如果避免使用using语句,仅在已有的try块的finally子句中调用Dispose(),还可以避免进行额外的缩进。
    注意:
    对于某些类来说,使用Close()要比Dispose()更富有逻辑性,例如,在处理文件或数据库连接时,就是这样。在这些情况下,常常实现IDisposable接口,再执行一个独立的Close()方法,来调用Dispose()。这种方法在类的使用上比较清晰,还支持C#提供的using语句。

    前面的章节讨论了类所使用的释放未托管资源的两种方式:
    ●         利用运行库强制执行的析构函数,但析构函数的执行是不确定的,而且,由于垃圾收集器的工作方式,它会给运行库增加不可接受的系统开销。
    ●         IDisposable接口提供了一种机制,允许类的用户控制释放资源的时间,但需要确保执行Dispose()。
    一般情况下,最好的方法是执行这两种机制,获得这两种机制的优点,克服其缺点。假定大多数程序员都能正确调用Dispose(),实现IDisposable接口,同时把析构函数作为一种安全的机制,以防没有调用Dispose()。下面是一个双重实现的例子:
    public class ResourceHolder : IDisposable
    {
         private bool isDispose = false;
          
          // 显示调用的Dispose方法
      public void Dispose() 
          {
               Dispose(true);
              GC.SuppressFinalize(this); 
           }

            // 实际的清除方法
      protected virtual void Dispose(bool disposing) 
           {
                if (!isDisposed)
              {
                   if (disposing) 
               { 
                         // 这里执行清除托管对象的操作.
                      }
                      // 这里执行清除非托管对象的操作
                }
        
            isDisposed=true;
          }

           // 析构函数 
          ~ResourceHolder()
          {
                Dispose (false);
          }
    }
    可以看出,Dispose()有第二个protected重载方法,它带一个bool参数,这是真正完成清理工作的方法。Dispose(bool)由析构函数和IDisposable.Dispose()调用。这个方式的重点是确保所有的清理代码都放在一个地方。
    传递给Dispose(bool)的参数表示Dispose(bool)是由析构函数调用,还是由IDisposable.Dispose()调用——Dispose(bool)不应从代码的其他地方调用,其原因是:
    ●         如果客户调用IDisposable.Dispose(),该客户就指定应清理所有与该对象相关的资源,包括托管和非托管的资源。
    ●         如果调用了析构函数,在原则上,所有的资源仍需要清理。但是在这种情况下,析构函数必须由垃圾收集器调用,而且不应访问其他托管的对象,因为我们不再能确定它们的状态了。在这种情况下,最好清理已知的未托管资源,希望引用的托管对象还有析构函数,执行自己的清理过程。
    isDispose成员变量表示对象是否已被删除,并允许确保不多次删除成员变量。这个简单的方法不是线程安全的,需要调用者确保在同一时刻只有一个线程调用方法。要求客户进行同步是一个合理的假定,在整个.NET类库中反复使用了这个假定(例如在集合类中)。最后,IDisposable.Dispose()包含一个对System.GC. SuppressFinalize()方法的调用。SuppressFinalize()方法则告诉垃圾收集器有一个类不再需要调用其析构函数了。因为Dispose()已经完成了所有需要的清理工作,所以析构函数不需要做任何工作。调用SuppressFinalize()就意味着垃圾收集器认为这个对象根本没有析构函数.

      正确理解以上内容,可以大大优化系统性能,及时释放不需要的数据,不能仅靠C#提供的自动回收机制,也需要程序员使用更灵活的办法!二者合一既能让程序运行飞快,也让系统更加稳定!

    托管程序中资源的释放问题

    .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提供了三种方法,也是最常见的三种,大致如下:

    <!--[if !supportLists]-->1. <!--[endif]-->析构函数;

    <!--[if !supportLists]-->2. <!--[endif]-->继承IDisposable接口,实现Dispose方法;

    <!--[if !supportLists]-->3. <!--[endif]-->提供Close方法。 

    经过前面的介绍,可以知道析构函数只能被GC来调用的,那么无法确定它什么时候被调用,因此用它作为资源的释放并不是很合理,因为资源释放不及时;但是为了防止资源泄漏,毕竟它会被GC调用,因此析构函数可以作为一个补救方法。而Close与Dispose这两种方法的区别在于,调用完了对象的Close方法后,此对象有可能被重新进行使用;而Dispose方法来说,此对象所占有的资源需要被标记为无用了,也就是此对象被销毁了,不能再被使用。例如,常见SqlConnection这个类,当调用完Close方法后,可以通过Open重新打开数据库连接,当彻底不用这个对象了就可以调用Dispose方法来标记此对象无用,等待GC回收。明白了这两种方法的意思后,大家在往自己的类中添加的接口时候,不要歪曲了这两者意思。 

    接下来说说这三个函数的调用时机,我用几个试验结果来进行说明,可能会使大家的印象更深。

    首先是这三种方法的实现,大致如下:

        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来说不属于真正意义上的释放,除了注意它需要显示被调用外,我在此对它不多说了。

    而对于析构函数而言,不是在对象离开作用域后立刻被执行,只有在关闭进程或者调用方法的时候才被调用,参看如下的代码运行结果。 

    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!

    -----------------------------

    注:GC为了提高回收的效率使用了Generation的概念,原理是这样的,第一次回收之前创建的对象属于Generation 0,之后,每次回收时这个Generation的号码就会向后挪一,也就是说,第二次回收时原来的Generation 0变成了Generation 1,而在第一次回收后和第二次回收前创建的对象将属于Generation 0。GC会先试着在属于Generation 0的对象中回收,因为这些是最新的,所以最有可能会被回收,比如一些函数中的局部变量在退出函数时就没有引用了(可被回收)。如果在Generation 0中回收了足够的内存,那么GC就不会再接着回收了,如果回收的还不够,那么GC就试着在Generation1中回收内存,如此往复。

  • 相关阅读:
    Excel技巧--按内容分列与合并
    Excel技巧--分类求和与空白批量填充
    Excel技巧--空白处补零
    Excel技巧--批量生成指定名称的文件夹
    Excel技巧--漏斗图让转化率直观明了
    Excel技巧--时尚的圆环比例图
    Excel技巧--让折线图带面积更直观生动
    Excel技巧--图表添加平均线为指标
    Excel技巧--使用温度计图让目标与实际对比更明显
    Linux之facl----设置文件访问控制列表(详解)
  • 原文地址:https://www.cnblogs.com/xsj1989/p/4617784.html
Copyright © 2020-2023  润新知