孙广东 2017.3.27
http://blog.csdn.NET/u010019717
内容摘取自 《CLR via C#》 第21章 托管堆和垃圾回收
同时也推荐查看我之前转载的一篇好文章 《对比Ruby和Python的垃圾回收》
资源包括包括:文件、内存缓冲区、网络连接等。
以下是访问一个资源所需的步骤:
CLR要求所有对象都从托管堆分配。进程初始化时,CLR划出一个地址空间区域作为托管堆,CLR还要维护一个指针,我把它称作NextObjPtr。该指针指向下一个对象在堆中的分配位置。
你的应用程序的内存受进程的虚拟地址空间的限制。32位进程最多能分配1.5GB,64位进程最多能分配8TB。
C#的new操作符导致CLR执行以下步骤:
应用程序调用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 的 可以看看这本书《