GC 垃圾回收机制

static void Main(string[] args)
{

    //被CLR托管的代码叫做托管代码,不被CLR管理的代码叫非托管代码

    // 堆:就是那些由new分配的内存块

    //凡是分配在栈里面的全是结构,结构是值类型。
    //栈空间里面的数据变量怎么被回收的呢?我们的代码是从上往下执行的,{}是一个作用域,分配在栈空间的变量会在执行完这个作用域之后立刻被我们是CLR回收。例如:
    {
        int a = 10;
        Program p = new Program();
         
    }
     //这个此时代码运行到这里,上面的{}这个作用域已经值为完毕了,里面的a变量已经被CLR自动回收,所以在这里我们是访问不到a的,因为a已经被回收销毁。
    // 我们看Program p = new Program()这段代码,这点代码是在栈里保存了一个变量p,它的值是指向堆里面的地址,这个地址是一个值类型。所以,此时p已经被回收,所以这里也访问不到p。 但是new Program() new出来的对象保存在堆内存中开辟一个空间中的,此时虽然p被回收了,但是保存在堆内存中的空间中的对象并不一定马上被GC回收。
    

    //那我们堆里面的数据是怎么回收,什么时候被回收呢?
    //答案是:分配在堆里面的对象,当没有任何变量指向(引用)它的时候,这个对象就被标记为“垃圾对象”,等待垃圾回收器回收
    //回过头来看这个p 这个p已经被销毁,所以此时,并没有任何变量指向(引用)到new Program()创建的对象,所以这个对象就会被标记为“垃圾对象”

    Program p2 = new Program();
    Program p3 = p2;
    p2 = null;//此时堆空间没有垃圾对象,因为p3指向了p2原来指向的对象
    p3 = null;//此时堆空间有垃圾对象,此时没有任何变量指向对象。
}


垃圾回收的原因

从计算机组成的角度来讲,所有的程序都是要驻留在内存中运行的。而内存是一个限制因素(大小)。除此之外,托管堆也有大小限制。【如果托管堆没有大小限制,那C#的执行速度要优于c了(托管堆的结构让它有比c运行时堆更快的对象分配速度)】因为地址空间和存储的限制因素,托管堆要通过垃圾回收机制,来维持它的正常运作,保证对象的分配,不会“内存溢出”。

垃圾回收的基本原理

GC会定时的清理堆空间中的垃圾对象,那么GC到底什么时候来清理堆空间里的垃圾对象呢?
答案是:程序员无法决定,CLR会自动控制,即便堆空间中存在“垃圾对象”这个垃圾对象也不一定立即被GC回收。

假设堆中有10000个对象,这10000个对象中可能存在“垃圾对象”那么GC是如何来清理这些垃圾对象的呢?
如果从头到尾扫描每个对象,看它是否是“垃圾对象”的话,这样GC的工作效率就很慢
那么垃圾回收器到底怎么回收这些垃圾对象呢?


垃圾回收分为两个阶段:  标记 --> 压缩
标记的过程,其实就是判断对象是否可达的过程。当所有的根都检查完毕后,堆中将包含可达(已标记)与不可达(未标记)对象。【注:可达表示正常对象,不可达表示垃圾对象】
标记完成后,进入压缩阶段。在这个阶段中,垃圾回收器线性的遍历堆,以寻找不可达对象的连续内存块。并把可达对象移动到这里以压缩堆。这个过程有点类似于磁盘空间的碎片整理。

如上图所示,绿色框表示可达对象,黄色框为不可达对象。不可达对象清除后,移动可达对象实现内存压缩(变得更紧凑)。

压缩之后,“指向这些对象的指针”的变量和CPU寄存器现在都会失效,垃圾回收器必须重新访问所有根,并修改它们来指向对象的新内存位置。这会造成显著的性能损失。这个损失也是托管堆的主要缺点。

基于以上特点,垃圾回收引发的回收算法也是一项研究课题。因为如果真等到托管堆满才开始执行垃圾回收,那就真的太“慢”了。

垃圾回收算法 - 分代(Generation)算法

代是CLR垃圾回收器采用的一种机制,它唯一的目的就是提升应用程序的性能。分代回收,速度显然快于回收整个堆。
CLR托管堆支持3代:第0代,第1代,第2代。第0代的空间约为256KB,第1代约为2M,第2代约为10M。新构造的对象会被分配到第0代,


实际CLR的代回收机制更加“智能”,如果新创建的对象生存周期很短,第0代垃圾也会立刻被垃圾回收器回收(不用等空间分配满)。另外,如果回收了第0代,发现还有很多对象“可达”,
并没有释放多少内存,就会增大第0代的预算至512KB,回收效果就会转变为:垃圾回收的次数将减少,但每次都会回收大量的内存。如果还没有释放多少内存,垃圾回收器将执行,完全回收(3代),如果还是不够,则会抛出“内存溢出”异常。

也就是说,垃圾回收器会根据回收内存的大小,动态的调整每一代的分配空间预算!达到自动优化!


总结


垃圾回收背后有这样一个基本的观念:编程语言(大多数的)似乎总能访问无限的内存。而开发者可以一直分配、分配再分配——像魔法一样,取之不尽用之不竭。

.NET垃圾回收器的基本工作原理是:通过最基本的标记清除原理,清除不可达对象;再像磁盘碎片整理一样压缩、整理可用内存;最后通过分代算法实现性能最优化。


代码验证

namespace GCApp
{
    public class Preson
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    class Program
    {
        static void Main(string[] args)
        {

            Preson p = new Preson();
            int gen = GC.GetGeneration(p);//获取指定对象的当前代数。返回0
            Console.WriteLine(gen); //输出:0


            GC.Collect(gen); //强制对第0代进行垃圾回收

            int twoGen = GC.GetGeneration(p); //经过上面的垃圾回收后,正常的对象(可达对象)被归为第1代
            Console.WriteLine(twoGen); //所以这里输出:1

            GC.Collect(twoGen);//强制对第1代进行垃圾回收

            int threeGen = GC.GetGeneration(p); //经过上面的垃圾回收后,正常的对象(可达对象)被归为第2代
            Console.WriteLine(threeGen); //所以这里输出:2


            int maxgem = GC.MaxGeneration; //获取系统支持的最大的代数。 永远返回2
            Console.WriteLine(maxgem);
            Console.ReadKey();
        }
    }
}

.Net 垃圾回收和大对象处理

CLR垃圾回收器根据所占空间大小划分对象。大对象和小对象的处理方式有很大区别。比如内存碎片整理 —— 在内存中移动大对象的成本是昂贵的,让我们研究一下垃圾回收器是如何处理大对象的,大对象对程序性能有哪些潜在的影响。

在.Net 1.0和2.0中,如果一个对象的大小超过85000byte,就认为这是一个大对象。这个数字是根据性能优化的经验得到的。当一个对象申请内存大小达到这个阀值,它就会被分配到大对象堆上。这意味着什么呢?要理解这个,我们需要理解.Net垃圾回收机制。

  如大多人所知道的,.Net GC是按照“代”来回收的。程序中的对象共有3代,0代、1代和2代,0代是最年轻的对象,2代对象存活的时间最长。GC按代回收垃圾也是出于性能考虑的;通常的对象都会在0代是被回收。例如,在一个asp.net程序中,和每一个请求相关的对象都应该在请求结束时回收掉。而没有被回收的对象会成为1代对象;也就是说1代对象是常驻内存对象和马上消亡对象之间的一个缓冲区。

  从代的角度看,大对象属于2代对象,因为只有在2代回收时才会处理大对象。当某代垃圾回收执行时,会同时执行更年轻代的垃圾回收。比如:当1代垃圾回收时会同时回收1代和0代的对象,当2代垃圾回收时会执行1代和0代的回收.

  代是垃圾回收器区分内存区域的逻辑视图。从物理存储角度看,对象分配在不同的托管堆上。一个托管堆(managed heap)是垃圾回收器从操作系统申请的内存区(通过调用windows api VirtualAlloc)。当CLR载入内存之后,会初始化两个托管堆,一个大对象堆(LOH –large object heap)和一个小对象对(SOH – small object heap)。

  内存分配请求就是将托管对象放到对应的托管堆上。如果对象的大小小于85000byte,它会被放置在SOH;否则会被放在LOH上。

  对于SOH,对象在执行一次垃圾回收之后,会进入到下一代。也就是说如果在第一次执行垃圾回收时,存活下来的对象会进入第二代,如果在第2次垃圾回收之后该对象仍然没有被当作垃圾回收掉,它就会成为2代对象;2代对象就是最老的对象不会在提升代数。

当触发垃圾回收时,垃圾回收器会在小对象堆做碎片整理,将存活下来的对象移动到一起。而对于大对象堆,由于移动内存的开销很大,CLR团队选择只是清除它们,将回收掉的对象组成一个列表,以便满足下次有大对象申请使用内存,相邻的垃圾对象会被合并成一块空闲的内存块。

 需要时时留意的是,直到.Net 4.0中也不会对大对象堆做碎片整理操作,将来也许会做。因此如果你要分配大对象并不想他们被移动,你可以使用fixed语句。

  如下小对象堆SOH的回收示意图


 上图中第一次垃圾回收之前有四个对象obj0-3;在第一垃圾回收之后obj1和obj3被回收了,同时obj2和obj0移动到一起了;在第二次垃圾回收之前有分配了三个对象obj4-6;在第二次执行垃圾回收之后obj2和obj5被回收了,obj4和obj6被移动到obj0旁边。


 可以看到在未执行垃圾回收之前,一共有四个对象obj0-3;第一次二代垃圾回收之后obj1和obj2被回收掉了,回收掉之后obj1和obj2所占空间被合并到了一起,在obj4申请分配内存时就把obj1和obj2回收后释放的空间分配给它了;同时留下了一块内存碎片。如果这个碎片的大小小于85000byte,那么这个碎片就在这个程序的生命周期中永远不能被再次利用了。

  如果大对象堆上没有足够的空闲内存容纳要申请的大对象空间,CLR首先会尝试向操作系统申请内存,如果申请失败,就会触发一次二代回收来尝试释放一些内存。

  在2代垃圾回收时,可以将不需要的内存通过VirtualFree交还给操作系统。交还的过程参见下图:


什么时候回收大对象呢? 

在讨论什么时候回收大对象之前先来看下普通的垃圾回收操作什么时机执行吧。垃圾回收在下列情况下发生:

  1. 申请的空间超过0代内存大小或者大对象堆的阀值,多数的托管堆垃圾回收在这种情况下发生

  2. 在程序代码中调用GC.Collect方法时;如果在调用GC.Collect方法是传入GC.MaxGeneration参数时,会执行所有代对象的垃圾回收,包括大对象堆的垃圾回收

  3. 操作系统内存不足时,当应用程序收到操作系统发出的高内存通知时

  4. 如果垃圾回收算法认为做二代回收是有收效时会触发二代垃圾回收

  5. 每一代对象堆的都有一个所占空间大小阀值的属性,当你分配对象到某一代,你增长了内存总量接近了该代的阀值,或者分配对象导致这一代的堆大小超过了堆阀值,就会发生一次垃圾回收。因此当你分配小对象或者大对象时,会对应消耗0代堆或者大对象堆的阀值。当垃圾回收器将对象代数提升到1代或者2代时,会消耗1、2代的阀值。在程序运行中这些阀值是动态变化的。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值