zoukankan      html  css  js  c++  java
  • 使用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的执行算法会很糟。

  • 相关阅读:
    MySQL的Limit 性能差?真的不能再用了?
    天天写order by,你知道Mysql底层如何执行吗?
    微信小程序 rich-text使用正则去除html中img标签中的css样式
    微信小程序开发加入版本更新提示并自动更新
    keepass 用户名显示星号的问题
    firebase/php-jwt使用openssl实现 RSA非对称加密
    Homstead ubuntu 系统pip3的安装
    sqlserver 重置自增列种子值 违反了 PRIMARY KEY 约束的处理
    ghost 安装系统出现EFI PART红色错误的问题
    在laravel 5.6中接管dingo/api 错误
  • 原文地址:https://www.cnblogs.com/coolkiss/p/1806382.html
Copyright © 2011-2022 走看看