• .net垃圾回收机制原理


      在.net 编程环境中,系统的资源分为托管资源和非托管资源。

      对于托管的资源的回收工作,是不需要人工干预回收的,而且你也无法干预他们的回收,所能够做的只是了解.net CLR如何做这些操作。也就是说对于您的应用程序创建的大多数对象,可以依靠 .NET Framework 的垃圾回收器隐式地执行所有必要的内存管理任务。
      资源分为两种,托管的内存资源,这是不需要我们操心的,系统已经为我们进行管理了;那么对于非托管的资源,这里再重申一下,就是Stream,数据库的连接,GDI+的相关对象,还有Com对象等等这些资源,需要我们手动去释放。

      对于非托管资源,您在应用程序中使用完这些非托管资源之后,必须显示的释放他们,例如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;
    
       // Pointer to an external unmanaged resource.
       private IntPtr handle;
    
       // Other managed resource this class uses.
       private Component Components;
          
          // 显示调用的Dispose方法
      public void Dispose()
          {
               Dispose(true);
              GC.SuppressFinalize(this);
           }
    
            // 实际的清除方法
      protected virtual void Dispose(bool disposing)
           {
                if (!isDisposed)
              {
                   if (disposing)
               {
                         // 这里执行清除托管对象的操作.
                      }
                      // 这里执行清除非托管对象的操作
                   CloseHandle(handle);
                   handle = IntPtr.Zero; 
                }
        
            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#提供的自动回收机制,也需要程序员使用更灵活的办法!二者合一既能让程序运行飞快,也让系统更加稳定!

  • 相关阅读:
    Codeforces Round #538 (Div. 2) F. Please, another Queries on Array?
    2021 ICPC济南 J Determinant
    牛客小白月赛43 F 全体集合
    The 2021 ICPC Asia Regionals Online Contest (II) L Euler Function
    C++文件操作详解
    利用VC实现图像的特殊显示效果 小楼machine
    Mixed mode assembly is built against version 'v2.0.50727' 解决方案 小楼machine
    如何在对话框资源从一个项目导入到另一个项目使用 Visual c + +.net 或 Visual c + + 2005 小楼machine
    灰度图像阈值化分割常见方法总结及VC实现 小楼machine
    Visual C++多媒体设计及图形、图像处理 小楼machine
  • 原文地址:https://www.cnblogs.com/longProgrammer/p/3157890.html
Copyright © 2020-2023  润新知