IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    [原]《读书笔记》C#/.Net 的托管堆和垃圾回收

    u010019717发表于 2017-03-27 20:08:23
    love 0

    孙广东  2017.3.27

    http://blog.csdn.NET/u010019717


     内容摘取自  《CLR via C#》     第21章 托管堆和垃圾回收


    同时也推荐查看我之前转载的一篇好文章  《对比Ruby和Python的垃圾回收》


    托管堆基础

    资源包括包括:文件、内存缓冲区、网络连接等。

    以下是访问一个资源所需的步骤:

    1. 调用IL指令newobj,为代表资源的类型分配内存(一般使用C# new操作符来完成)。
    2. 初始化内存,设置资源的初始状态并使资源可用。类型的实例构造器负责设置初始状态。
    3. 访问类型的成员来使用资源(有必要可以重复)。
    4. 摧毁资源的状态以进行清理。(为了简化编程,开发人员经常使用的大多数类型都不需要这个步骤)。
    5. 释放内存。垃圾回收器独自负责这一步。

     

     

    托管堆分配资源

               CLR要求所有对象都从托管堆分配。进程初始化时,CLR划出一个地址空间区域作为托管堆,CLR还要维护一个指针,我把它称作NextObjPtr。该指针指向下一个对象在堆中的分配位置。

                 你的应用程序的内存受进程的虚拟地址空间的限制。32位进程最多能分配1.5GB,64位进程最多能分配8TB。

     

     

    C#的new操作符导致CLR执行以下步骤:

    1. 计算类型的字段(以及从基类型继承的字段)所需的字节数。
    2. 加上对象的开销所需的字节数。每个对象都有两个开销字段:类型对象指针和同步块索引。对于32位应用程序,这两个字段各自需要32位,所以每个对象都要8字节。对于64位应用程序,这两个字段各自需要64位,所以每个对象要增加16个字节。
    3. CLR检查区域中是否有分配对象所需的字节数。如果托管堆有足够的可用空间,就在NetxObjPtr指针指向的地址处放入对象,为对象分配的字节会被清零。接着调用类型的构造器(为this参数传递NextObjPtr),new操作符返回对象引用。就在返回这个对象引用之前,NextObjPtr指针的值会加上这个对象占用的字节数来得到一个新值,即下个对象放入托管堆时的地址。

     

    垃圾回收算法

              应用程序调用new操作符创建对象时,可能没有足够地址空间来分配该对象,发现空间不够,CLR就执行垃圾回收。

              至于对象生存期的管理,有的系统采用的是某种引用计数算法。在这种系统中,堆上的每个对象都维护着一个内存字段来统计程序中多少“部分”正在使用对象。随着每一“部分”到达代码中某个不再需要对象的地方,就递减对象的计算字段。计数字段成0,对象就可以从内存中删除了。许多引用计数系统最大的问题是处理不好循环引用。

                 鉴于引用计数垃圾回收器算法存在的问题,CLR改为使用一种引用跟踪算法。引用跟踪算法中关心引用类型的变量,因为只有这种变量才能引用堆上的对象;值类型变量直接包含值类型实例。引用类型变量可在许多场合使用,包括静态和实例字段,或者方法的参数和局部变量。我们将所有引用类型的变量都称为根。

               1、CLR开始GC时,首先暂停进程中的所有线程。这样可以防止线程在CLR检查期间访问对象并更改其状态。      2、然后,CLR进入GC的标记阶段。在这个阶段,CLR遍历堆中的所有对象,将同步块索引字段中的一位设为0。这表明所有对象都应删除。   3、然后,CLR检查所有的活动根,查看它们引用了哪些对象。这正是CLR的GC称为引用跟踪GC的原因。如果一个根包含NULL,CLR忽略这个根并继续检查下一个根。

               4、任何根如果引用了堆上的对象,CLR都会标记那个对象,也就是将该对象的同步块索引中的位设为1, 标记过程会持续,直至应用程序的所有跟所有检查完毕。       

               5、检查完毕后,堆中的对象要么已标记,要么未标记。已标记的对象不能被垃圾回收,因为至少有一个根在引用它。我们说这种对象是可达的。因为应用程序代码可通过仍在引用它的变量抵达(访问)它。未标记的对象是不可达的。因为应用程序中不存在使对象能被再次访问的根。

                 6、CLR知道哪些对象可以幸存,哪些可以删除后,就进入GC的压缩(不是那个压缩,类似于碎片整理)阶段。在这个阶段。CLR对堆中标记的对象进行“乾坤大挪移”。

                7、在内存中移动了对象之后有一个问题亟待解决。引用幸存对象的根现在引用的还是对象最初在内存中的位置,而非移动后的位置。被暂停的线程恢复执行时,将访问旧的内存位置,会造成内存损坏。这显然是不能容忍的,所以作为压缩阶段的一部分,CLR还要从每个根减去所引用对象在内存中偏移的字节数。这样就能保证每个根还是引用和之前一样的对象,只是对象在内存中变换了位置。

                8、压缩阶段完成后,CLR恢复应用程序的所有线程。

     

    重要提示:   静态字段引用的对象一直存在,直到用于加载类型的AppDomain卸载为止。内存泄漏的一个常见原因就是让静态字段引用某个集合对象,然后不停地往集合添加数据项。静态字段使集合对象一直存活,而集合对象使所有数据项一直存活。因此应该尽量避免使用静态字段。(或者参照前面的玩法,当我们不用静态变量的时候,可以立马置为null,那么垃圾就会被回收)。

     

    代:提升性能

     

    CLR的GC是基于代的垃圾回收器。它对代码做了如下假设:

    • 对象越新,生存期越短
    • 对象越老,生存期越长
    • 回收堆的一部分,速度快于回收整个堆

              

    第一个假设是越新的对象活的越短。因此,第0代包含跟多垃圾的可能性很大,能回收更多的内存。由于忽略了第1代中的对象,所以加快了垃圾回收速度。

     

    第二个假设越老的对象活的越长。也就是说,第1代对象在应用程序中很有可能继续可达(没被回收)的。如果垃圾回收器检查第1代中的对象,很有可能找不到多少垃圾。

     

    由于第0代已满,所以必须开始垃圾回收。但这一次垃圾回收器发现第1代占用了太多内存,以至于用完了预算。 由于前几次对第0代进行回收时,第1代可能已经有许多对象变得不可达(该回收)。所以这次垃圾回收器决定检查第1代和第0代的所有对象。 两代都被垃圾回收后,  就出现了第2代了。 空的是0代, 0代幸存者变为1代,1代幸存者变为2代。

     

               托管堆只支持三代:第0代, 第1代,第2代。

     

    CLR 的垃圾回收器是自动调节的:

    1、如果垃圾回收器发现在回收第0代后存活下来的对象很少,就可能减少第0代的预算。已分配空间的减少意味着垃圾回收将更频繁地发生。

    2、另一方面,如果垃圾回收器回收了第0代,发现还有很多对象存活,没有多少内存被回收就会增加第0代的预算。

    3、垃圾回收器用类似的  启发式算法 调整第1代 和 第2代的预算。

     

     

    垃圾回收触发条件

     

    1、CLR在检测第0代超过预算时会触发一次GC,这是GC最常见的触发条件,还有其它的触发如下:

    2、代码显示调用System.GC的静态Collect方法,  大多时候都要避免调用这个方法;最好让垃圾回收器自行斟酌执行,让它根据应用程序的行为调整各个代的预算。

    3、Windows报告低内存情况

    4、CLR正在卸载AppDomain

    5、CLR正在关闭

     

    还想深入了解GC 的 可以看看这本书《

    垃圾回收的算法与实现

    》


沪ICP备19023445号-2号
友情链接