13

.NET对象清理:垃圾回收和资源清理

 4 years ago
source link: http://developer.51cto.com/art/202003/612364.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

fAvY3my.jpg!web

【51CTO.com原创稿件】 在 .NET 中垃圾回收和资源清理是重中之重的内容,也是所有程序都必须用到的机制,但是有很大一部分开发人员并不知道垃圾回收和资源清理的原理。那么,我将通过这篇文章向各位读者详细讲解一下垃圾回收和资源清理。

一、垃圾回收

.NET中垃圾回收是运行时的核心功能,它的作用是回收不再被引用的对象所占用的内存。这里我们要注意垃圾回收器只回收内存资源而不处理其他资源。此外垃圾回收器是根据是否存在任何引用来决定要清理那些东西,也就是说垃圾回收器处理的是不被引用的引用对象,并且只能回收堆上的内存。

  1. 简述 在 .NET 中垃圾回收的很多细节都和 CLI 有关,我们常用的 Microsoft.NET 框架中实现垃圾回收的算法是 mark-and-compact 算法 。当每次一次垃圾回收周期开始时,它会查找对象的所有根引用(一般来说根引用来自静态变量、CPU寄存器和局部变量或参数实例的任何引用)。基于查找到的所有根引用,垃圾回收器就可以遍历每个根引用标识的树形结构,并递归确定每个根引用指向的对象,进而识别出所有可达对象。

    当执行垃圾回收时,垃圾回收器会将所有可达对象一个挨一个的放在一起,这样就可以覆盖不可达对象所占用的内存。为了定位和移动可达对象,进程中所有托管线程都会在垃圾回收期间暂停运行,这样就可以保证垃圾回收器在运行期间维持状态一致性。虽然这么做会造成应用程序短暂停止工作,但是一般来说只要垃圾回收周期不是特别长,这个短暂的停止工作是很难发觉的。在我们开发时有时可能不希望在运行一些代码段时执行垃圾回收,这时我们可以在代码段之前使用 System.GC 对象所包含的 Collect 方法来让垃圾回收暂时跳过这些代码。当然这么做是不会阻止垃圾回收运行的,只是减少了这部分代码可能被回收的概率,但是这里有一个前提条件:代码段执行期间不会发生内存被大量消耗使用的情况。 在 .NET 中垃圾回收有一个特别的地方,就是并非所有的垃圾都会在一个垃圾回收周期内被回收。这是为什么呢?因为在 .NET 垃圾回收器中有一个名字叫 generation 的概念,翻译成中文就是 。它会清理那些生存时间较短的对象,那些在一次垃圾回收周期中存活下来的对象会降低清理频率。也就是说当一个对象在一次垃圾回收周期中存活下来,那么它将会被移动到下一代中,如果它又在一次垃圾回收周期中存活下来,那么它将被移动到最后一代,也就是第二代(为什么是第二代呢?因为 .NET 垃圾回收机制中代是从 0 开始的),第零代清理速度最快,第二代清理速度最慢。
  2. 弱引用 弱引用这个名词很少有开发人员听过,所谓的弱引用是为创建起来开销很高并且维护成本也很大的对象而设计的。它不阻止垃圾回收器对对象的回收,但会维持一个引用,进而可以在被垃圾回收器回收之前可以重用。例如我们从数据库中查询一个庞大的数据列表向用户展示,如果没有使用弱引用当用户关闭了这个列表,那么垃圾回收器就有很大可能将它回收,那么当用户再次查看这个列表时,程序又需要从数据库查询并加载出来,这种操作成本是很高昂的。如果使用了如引用,每次请求列表时代码首先检查列表是否被清除,如果没有被清除就直接将列表展示给用户,如果被清除了就从数据库查询并展示给用户,这就相当于对象在内存中进行了缓存。如果开发人员认为对象应该进行弱引用,那么就可以把这个对象赋值给 System.WeakReference 。下面我们来看一个弱认证的简单例子:

    WeakReference Data; 
    public FileStream Date() 
    { 
       FileStream fs= (FileStream)Data.Target; 
       if(data!=null) 
      { 
           return data; 
      } 
       // more code 
       Data.Target=data; 
       return data; 
    } 
    

    上面的代码是一个标准的创建弱引用的代码,我们可以看到在代码中对变量 data 进行了 null 判断,我们可以通过这个判断来检查垃圾回收器是否将其回收。这里还有一个关键代码 FileStream fs= (FileStream)Data.Target; 这里将弱引用赋值给了强引用,这样可以避免在检查 null 后和访问数据前,发生垃圾回收器回收弱引用。

二、资源清理

在前面一小节开头我们说过垃圾回收之回收内存中的对象,那么如果我们需要回收其他资源呢,例如数据库连接、句柄、外部设备。这时我们就需要用到资源清理。

终结器 终结器是一个允许开发人员通过代码来清理类资源的东西。终结器最大的特征是它不能在代码中显式调用,只有垃圾回收器负责对对象的实例调用终结器,因此开发人员无法在编译时确定终结器在何时执行,只能够确定终结器时对象中最后一次被调用的地方。 终结器的定义也很简单,只需要在类名之前加一个 ~ 符号即可。

class Demo 
{ 
   public Demo(string name) 
  { 
       //more code 
  } 
    
   ~Demo() 
  { 
       Close(); 
  } 
   public void Close() 
  { 
       //more code 
  } 
   //more code 
} 

上述代码我们就定义了一个简单的终结器,我们定义终结器的时候需要注意以下四点:

因为终结器是在自己的线程中执行的,因此如果终结器中存在一个未处理的异常就会很难诊断发现,因为造成异常的情况并不清晰透明。所以我们必须避免在终结器中引发异常。

  • 终结器是不允许传递任何参数的,也不能重载它;
  • 因为它是被垃圾回收器所调用,因此给终结器加上访问修饰符是毫无意义的;
  • 如果父类中存在终结器,那么将会作为子类终结器的一部分被自动调用;
  • 终结器必须显示的释放资源。
  1. using 虽然终结器可以帮助我们在忘记显式调用必要清理代码的时候执行清理,但是因为终结器的运行存在不确定性,因此我们只能将它作为备用机制。正常情况下我们可以使用 using C# 中的 IDisposable 接口的 Dispose 方法为我们提供了实现细节。我们先来看一段代码。

    class Demo 
    { 
       MyFileStream fs =new myFileStram(); 
       //more code 
       fs.Dispose(); 
       //more code 
    } 
    class MyFileStream:IDisposable 
    { 
       public MyFileStream(string path) 
      { 
           //more code 
      } 
       //more code 
       ~MyFileStream 
      { 
           Dispose(false); 
      } 
       public void Close() 
      { 
           Dispose(); 
      } 
       public void Dispose() 
      { 
           Dispose(true); 
           System.GC.SuppressFinalize(); 
      } 
       public void Dispose(bool para) 
      { 
           // more code 
      } 
    } 
    

    上述代码中我们显式调用了 MyFileStream 类的 Dispose 方法。 Dispose 方法主要用来清理已经用过的资源,但是这里存在一个问题,当我们调用 Dispose 方法时有可能会发生异常,这时我们就无法正确调用 Dispose 方法了,为了避免这个问题我们需要加入 try..finally 块。但是我们无法保证开发人员每次都会写 try...finally ,这时我们可以使用 C# 提供的 using 语句,我们将上面的调用代码修改一下:

    class Demo 
    { 
       using(MyFileStream fs =new myFileStram()) 
      { 
           //more code 
      } 
    } 
    

    这段代码最终生成的 CIL 代码和使用 try...finally 块生成的代码完全一样。

  2. 垃圾回收、终结和 IDisposable 在上一小节的代码中我们看到在 Dispose 方法中我们调用了 System.GC.SuppressFinalize(); ,它的作用是从终结队列中移除 MyFileStream 实例。因为所有清理都在Dispose 方法中完成了,而不是等着终结器执行。如果不调用 System.GC.SuppressFinalize() 方法实例将会一直在终结队列中,只有当终结方法被调用之后才能在垃圾回收器中被回收,那么这就造成了托管资源垃圾回收处理时间的延迟。 Dispose 方法中调用了 Dispose(bool para) 方法,在这个方法里我们可以清理资源并阻止终结器。其次,我们定义了 Close 方法来调用 Dispose(bool para) 方法,这样终结器就可以调用 Dispose(bool para) 方法来关闭释放资源。针对前一小结的代码需要有如下几点注意:

    在某些特殊情况下垃圾回收的对象有可能会被无意中重新引用一个待终结的对象。这样,被重新引用的对象就不再是不可访问的,所以不能当作垃圾被回收掉。假如对象的终结方法已经运行,那么除非显式标记为要进行终结,否则终结方法不一定会再次运行。

  • 只针对开销大,成本高的对象实现终结器;
  • 如果类存在终结器那么就必须实现 IDisposable ;
  • 不要在终结器中抛出异常;
  • 在 Dispose 方法中必须调用 System.GC.SuppressFinalize ;
  • 保证 Dispose 可以被重用;
  • 保证 Dispose 方法的简单性;
  • 不能在终结器中调用未被终结的其他对象;
  • 如果父类存在终结器,再重写时必须调用父类终结器;
  • 调用 Dispose 方法之后,将对象设为不可用。

三、小结

这篇文章详细讲解了垃圾回收和资源清理相关的知识,对于部分开发人员来说这部分知识可能晦涩难懂,但是只要在实际项目中上手使用,我相信就可以很快的掌握和理解。

作者介绍:

朱钢,笔名喵叔,国内某技术博客认证专家,.NET高级开发工程师,7年一线开发经验,参与过电子政务系统和AI客服系统的开发,以及互联网招聘网站的架构设计,目前就职于一家初创公司,从事企业级安全监控系统的开发。

【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】

【责任编辑:庞桂玉 TEL:(010)68476606】


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK