使用finalize/dispose 模式提高GC性能(翻译)

今天有继续翻译啦,哈哈,之前看《你必须了解的.net》就了解过一些关于GC回收的机制,通过翻译本文,有增加了一些了解,欢迎大家拍砖:-)

原文链接:

http://www.codeproject.com/KB/aspnet/DONETBestPracticeNo2.aspx#Conclusion%20about%20generations

 

使用finalize/dispose 提高GC性能

 

本文是否值得继续阅读?

介绍和目的

假定

感谢 Mr. Jeffrey Richter 和 Peter Sollich

GC--无名英雄

“代”算法 --今天,昨天,前天

“代”是如何提高优化性能

“代”的总结

使用“finalize/destructor”会导致第一代和二代的对象增加

使用Dispose来替换析构函数

如果程序员忘记调用dispose方法?

总结

 

本文是否值得继续阅读?

通过这篇文章,你可以了解到如何使用finalize/dispose方法来提高GC的性能。下图显示的就是我们这篇文章所要达到的目标。

介绍和目的

如果任何一个程序员最好释放非托管资源最好的方法是什么? 70%的人会说析构函数。虽然析构函数看起来
释放资源的好地方,但是它会引发重大的性能和内存消耗。在析构函数中编写释放资源代码会导致两次访问GC,并多次影响性能。

为了验证上面的理论,我们会先开始一点理论知识,然后在讲析构函数是如何影响GC算法的性能。因此我们先来理解“代”的概念,
然后再看看finalize/dispose方法。

我敢肯定这篇文章会改变你对析构函数,dispose和finalize的看法。

 

假定

这篇文章使用CLR profiler来检测GC的工作。如果你未了解过CLR profiler,请先阅读之前的那篇文章。然后再继续阅读本文。

 

感谢 Mr. Jeffrey Richter 和 Peter Sollich

在开始本文前,首先要改写 Mr. Jeffrey Richter深入的结束了GC算法是如何运行的。他写过两篇关于GC工作的精彩文章。
本来我想贴出MSDN杂志中的这两篇文章,但是处于某些原因未能在MSDN中发现。因此,我贴出另外一个非官方地址,你可以
中这个地址下载pdf文档:http://www.cs.inf.ethz.ch/ssw/files/GC_in_NET.pdf

同事感谢CLR性能优化架构师Mr. Peter Sollich 编写了如此详细的CLR profiler帮助文档。如果你安装了CLR profiler,别忘记
阅读一下该详细文档。在本文中我们将使用CLR profiler来查看GC是如何受finalize影响的。

非常感谢你们。如果没有阅读你们的文章,我不可能完成本文。如果你们有路过,请留下一些评论。

 

GC--无名英雄

刚刚介绍中说到,在析构函数清除资源会导致两次访问GC。许多程序员可能会辩解说:“我们是不是真的需要担心在后台运行的GC?”.
是的,事实上我们不必担心GC的工作,如果我们代码写的正确的话。GC有最佳的算法来保证你的应用程序不受影响。但是很多时候,你
的代码和代码中分配/清除内存资源经常会影响到GC算法,有时候,这就给GC的性能带来坏影响,同时也影响到你应用程序的性能。

我们先来理解一下gc分配内存和回收内存的区别。

假设我们有三个类,类“A”引用了类“B”,类“B”引用了类“C”

首次运行程序时会首先在内存为应用程序申请内存地址。当程序创建这三个对象后,他们功过内存地址来指向内存堆。从下图中你可以看到
对象创建前后的内存分配情况。如果再创建一个D对象,那么他将会被分配到C对象的后面。

在GC内部维护着一个对象图,用于判断对象是否可达。所有对象属于主程序的根对象。根对象同时也维护着哪个对象分配到哪个地址。
因此,如果一个对象包含了另外一个对象,那么该对象保存了另外一个对象的地址。例如,对象A包含了对象B,因此对象A也保存了对象B的地址。

现在假设A从内存中移除,那么A的内存地址分配给了B,而B的则给了C。此时内存内部分配情况如下图所示:

由于对象的地址更新了,GC也必须保证其内部对象图也更新到最新内存地址。因此,对象变成了下面这样子。现在GC需要做大量的工作来确保
对象从对象图中移除以及更新对象树中现有对象的地址。

一个程序的对象图不仅有它自定义对象同时也有.net内部对象。这些对象地址也需要更新。.NET运行时有大量的对象。例如,下面显示的是一个简单
“hello world”控制台应用程序的对象数量。对象数大概有1000个,更新指针遍历这些对象是一项大规模的任务。

 

“代”算法 -- 今天,昨天和前天

GC使用了代的概念来提高性能。代的概念是基于人类心理学的处理问题方式。下面是列出一些关于人类如何处理问题和GC同样也是怎样处理的:


如果你今年决定做某事,那么最后可能会完成这件事情。
如果某事是昨天未完成的,那么该件事很有可能是不重要的,通常也可以推迟完成。
如果某事是前天遗留下来的,那么该事很大可能被是永久推迟的。

 

GC也同样这么认为,因此有以下假设:

 

如果新对象,那么它的生命周期是短的。
如果旧对象,那么他可能需要更长的生命周期。

 

于是GC支持三代(Generation 0, Generation 1 和 Generation 2)

第0代里是所有新创建的对象。当程序创建对象时,这些对象首先会被放到第0代中。当第0带资源用完时,GC就会释放一些内存资源。
因此GC开始建立代图,然后清除程序不再使用的对象。如果GC不能清除第0代对象,那么GC会将它移动到第1代。如果紧接着也不可以
从第1代中清除,那么对象将会被移动到第2代。.net运行时最高只能支持到2代。

下图是显示通过CLR profiler来看代对象的,如果你没使用过CLR profiler,你可以阅读上一篇文章


“代”是如何提高优化性能

由于所有的对象都包含在代关系中,因此GC可以决定清除哪一代的对象。不知道你是否记得我们之前谈到,GC通过对象的年龄来清除对象。
GC假定所有新的对象的生命周期都是很短的。也就是说,GC大部分是遍历第0代对象,而不是其它代对象。

如果清除0代资源还不够,那么它将会继续遍历第1代和其它代对象。这个算法很大程度的提高了GC的性能。

 

“代”的总结

第1代和第2代存在大量对象则说明没有优化内存分配。
增大第1代和第2代内容会导致GC性能下降。

 

使用“finalize/destructor”会导致第一代和二代的对象增加

C#编译器会将析构函数重命名为Finalize。如果你用IDASM来查看程序的IL代码,你会发现析构函数被重命名为Finalize。
因此我们理解一下为什么引入析构函数会导致gen 1和gen 2有更多的对象,下面是程序具体的流程:

当新对象创建后会放到第0代。
当0代对象满了后,GC启动试图清除一些内存。
如果对象不再被使用,而且没有析构函数,那么直接清除该对象。
如果对象有finalize方法,则把这些对象放到“finalization”队列
如果对象是可到达的,那么它会被移动到“Freachable”队列。如果对象不可达,则内存会被重置。
GC在这个迭代中完成。
下一次GC会首先遍历Freachable对象检测所有对象是否都是可以达的,如果对象不可达,则Freachable的内存会被回收。

换句话说:如果对象有析构函数,那么它会在内存待更多时间。
我们来看看下面的例子,下面是一个有析构函数的简单类。

 

class clsMyClass 

  public clsMyClass()
  { 
  }
  ~clsMyClass()
  {
}
}

 

 

我们通过循环创建100 * 10000个对象,同时使用CLR profiler监测。

 

for (int i = 0; i < 100 * 10000; i++)
{
  clsMyClass obj 
= new clsMyClass();
}

 

 

如果你查看CLR profiler的内存地址报告,你可以看到很多对象都在gen 1中

现在我们将析构函数移除,同样也传进100*10000个对象。

 

class clsMyClass 

  public clsMyClass()
  { 
  }
}

 

你可以看到第0代增长了一定的数量,而第1代和第2代就变少了。

如果我们看一下对比图,你可以看到下面的图片。

 

使用Dispose来替换析构函数

我们可以通过实现IDisposable接口中的Dispose方法来实现清理代码,而避免使用析构函数,在Dispose方法编写清除代码,
同时调用 SuppressFinalize方法,如下面代码段所示。‘SuppressFinalize’ 方法告诉GC不去调用Finalize方法。因此GC才不会重复调用。

 

class clsMyClass : IDisposable

  public clsMyClass()
  { 
  }
  ~clsMyClass()
  {
  }

  public void Dispose()
  {
    GC.SuppressFinalize(
this);
  } 
}

 

 

客户端必须确保调用了Dispose方法,如下所示:

 

for (int i = 0; i < 100 ; i++)
{
  clsMyClass obj 
= new clsMyClass();
  obj.Dispose(); 
}

 

 

下面的对比图可以看到使用析构函数和Dispose函数的两种情况。从标识中我们可以看到gen 0具有更好的内存分配情况。

 

如果程序员忘记调用dispose方法?(第一次就忘记写demo代码时候就忘记调用Dispose了,:-))

事实情况并不是完美的,我们不可能确保客户端都能正确调用Dispose方法。因此我们可以使用Finalize/Dispose模式来解释接下来节的内容。

更详细的实施模式可以访问:http://msdn.microsoft.com/en-us/library/b1yfkh5e(VS.71).aspx

下面是如果使用 finalize/dispose 模式的方法:

 

class clsMyClass : IDisposable

  public clsMyClass()
  {

  }

  ~clsMyClass()
  {
    // In case the client forgets to call
    // Dispose , destructor will be invoked for
  Dispose(false);
  }
  protected virtual void Dispose(bool disposing)
  {
    if (disposing)
    {
      // Free managed objects.
    }
    // Free unmanaged objects

  }

  public void Dispose()
  {
    Dispose(
true);
    // Ensure that the destructor is not called
    GC.SuppressFinalize(this);
  } 
}

 

代码解析:

 

我们定义了一个带bool参数的Dispose方法,这个参数用于判断是通过Dispose调用还是析构函数调用。如果是通过“Dispose”方法调用
的则可以释放托管和非托管资源。
如果是通过析构函数调用,那么我们只释放非托管资源。
在Dispose方法中,我们调用SuppressFinalize方法,同时通过true调用Dispose方法。
在析构函数中,我们通过false调用Dispose方法,换句话说我们假定GC处理托管资源,而析构函数用于处理非托管资源。
换句话说,如果客户端忘记调用Dispose方法,那么析构函数会结果清除非托管资源的任务。

 

总结

不要在你的类中使用空的析构函数
如果你需要使用finalize/dispose模式来清除资源,必须调用SupressFinalize方法
如果类中暴露了Dispose方法,客户端必须确保调用该方法。
程序中更多的对象应该存在于Gen 0,而不是Gen 1 和 Gen 2,如果更多对象在 Gen 1和Gen 2,那么说明GC的执行算法会很糟。

 

 

posted @ 2010-08-23 13:55  Chris Cheung  阅读(963)  评论(0编辑  收藏  举报