赞
踩
在讨论垃圾回收之前,需要明白一个重要的事情,空间是怎么被分配出去的。在进程初始化时,CLR会保留一块连续的地址空间(托管堆),托管堆中维护着一个指针,称之为NextObjPtr,它指向下一个对象在堆中的分配位置。当我们在C#中调用new关键字的时候,编译器会自动生成IL指令newobj,该指令将导致CLR执行以下步骤:
当进行非托管编程时,垃圾回收便成为了一个非常令我们头疼的问题。因为经常忘记释放已成为垃圾的对象空间,会造成严重的内存泄漏。幸好的是托管编程把我们解决了这个问题,通过垃圾回收,我们现在不必追踪内存的使用,也不用知道在什么时候释放内存。GC会为我们做好一切。
首先我们先明确“根”的概念:
根,每个应用程序都包含一组根。每个根都是一个储存位置,其中包含指向引用类型对象的一个指针。该指针要么引用托管
堆中的一个对象,要么为null。
可以简单认为,任何引用类型的变量都被认为是根,值类型永远不会是根。
它实际上是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。
许多引用计算最大的问题就是不好处理循环引用,简单来说就是在如下代码中,2个对象的引用计数将永远是1:
public class ObjMy { public object myRef; } public class Program { public static void Main(string[] args) { var a = new ObjMy(); var b = new ObjMy(); a.myRef = b; b.myRef = a; a = null; b = null; } }
当出现循环引用时,2个对象永远不会删除,即使本身不在被需要。
引用跟踪算法只关心引用“类型的变量”(根),因为只有这种变量才能引用堆上的对象。
CLR所有对象都标记完成后,就进入GC的压缩阶段。
在GC压缩过程中,所有的幸存对象都将在内存中紧挨在一起,这有几个好处。其一,恢复了引用的“局部化”,减小了应用程序的工作集,提升了将来访问这些对象时的性能;其二,解决了原生堆的空间碎片化问题。
当然,GC压缩会使得对象在内存中地址产生变化,在压缩的最后一阶段,CLR会计算每个根的新的地址,保证每个根的引用还是和之前一样的对象。
如果CLR在一次GC后回收不了内存,而且进程中也没有更多的空间来分配新的GC区域,就说明该进程的内存已被耗尽了。此时,new操作符会抛出OutOfMemoryException异常。
CLR的GC是“基于代的垃圾回收器”,它对代码做出以下三种假设:
对象越新,生存期越短
对象越老,生成期越长
回收堆的一部分,速度快于回收整个堆
CLR在初始化时是不包含对象的,添加到堆中的对象被称为第0代对象。它还会为第0代对象选择一个预算(以KB为单位)。如果分配一个新对象造成第0代超过预算,就会启动一次垃圾回收。在垃圾回收后存活下来的对象会升代,成为第1代对象。
在有第1代对象的前提下,进行垃圾回收时。垃圾回收器会利用JIT编译器内部的一个机制,检查老对象是否有新数据被写入,如果有,才会对字段发生变化的老对象进行检查判断是否引用了第0代中任何新对象。
CLR初始化时,除了会为第0代对象选择预算,也会为第1代对象选择预算。
开始一次垃圾回收时,垃圾回收器还会检查第1代占用了多少内存。在上例中,由于第1代对象远少于预算,所以会忽略对第1代中对象的检查。
托管堆只支持三代:第0代,第1代,第2代。
CLR的垃圾回收器的自调节:
如果垃圾回收器在回收垃圾后存活下来的对象很少,就可能减少第0代的预算。这意味着垃圾回收将更频繁,但垃圾回收器
每次做的事情也减少了,这意味着减小了进程的工作集。另一方面,如果垃圾回收器回收了第0代,还是有很多对象存活,
会增大第0代的预算。
除了第0代外,1,2代也会通过类似的启发式算法调整预算。从而提升了应用程序的整体性能。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。