C#的GC垃圾回收(初步了解)

垃圾回收

空间分配

在讨论垃圾回收之前,需要明白一个重要的事情,空间是怎么被分配出去的。在进程初始化时,CLR会保留一块连续的地址空间(托管堆),托管堆中维护着一个指针,称之为NextObjPtr,它指向下一个对象在堆中的分配位置。当我们在C#中调用new关键字的时候,编译器会自动生成IL指令newobj,该指令将导致CLR执行以下步骤:

  1. 计算类型(及其所有基类型)的字段,类型对象指针和同步块索引所需要的字节数。
  2. 检查是否有足够的空间储存,有的话,将对象储存在NextObjPtr指向的空间中,调用类型实例的实例构造器进行初始化,newObj指令返回对象的地址
  3. 在地址返回前,NetxObjPtr指针的值会加上对象占据的字节数,指向下一个对象占据的空间。
    在这里插入图片描述

托管还是非托管?

当进行非托管编程时,垃圾回收便成为了一个非常令我们头疼的问题。因为经常忘记释放已成为垃圾的对象空间,会造成严重的内存泄漏。幸好的是托管编程把我们解决了这个问题,通过垃圾回收,我们现在不必追踪内存的使用,也不用知道在什么时候释放内存。GC会为我们做好一切。

托管堆的优点

  1. 分配速度快,从托管堆中分配对象的速度几乎可以合从线程栈分配内存媲美
  2. 在托管堆中,连续分配的对象可以确保在内存中是连续的
  3. 托管堆会自动进行垃圾回收,当NextObjPtr加上对象需要的字节数超过地址空间的末尾,会执行一次垃圾回收。

垃圾回收算法

首先我们先明确“根”的概念:
根,每个应用程序都包含一组根。每个根都是一个储存位置,其中包含指向引用类型对象的一个指针。该指针要么引用托管
堆中的一个对象,要么为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个对象永远不会删除,即使本身不在被需要。

引用跟踪算法

引用跟踪算法只关心引用“类型的变量”(根),因为只有这种变量才能引用堆上的对象。

  1. CLR开始GC时,会暂停进程中所有线程。防止线程在CLR检查期间访问对象并更改其状态。
  2. CLR进入GC标记阶段,会遍历托管堆中所有对象,将同步块索引字段中的一位设为0,表明索引对象都应该删除。然后CLR检查“活动根”(不为null的根),查看它们引用了哪些对象。
  3. 任何根如果引用了堆上的对象,CLR都会标记那个对象,将同步块索引字段设为1。
  4. 一个对象被标记后,CLR才会查看对象其中的根,标记对象中的活动根,这样就避免引用计数算法的循环引用问题。
  5. 检查完毕后,已标记的对象被称为“可达的”,未标记对象被称为“不可达的”,因为应用程序中不在存在使对象能够被访问的根。

CLR所有对象都标记完成后,就进入GC的压缩阶段。

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代也会通过类似的启发式算法调整预算。从而提升了应用程序的整体性能。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值