浅析CLR的GC(垃圾回收器)

 

文章目录:

  1. 了解托管堆和GC
  2. GC高效的处理方式—代
  3. 特殊类型的清理
  4. 手动监控和控制对象生命周期

一、了解托管堆和GC

  在面向对象环境中,每个类型都表明了一种资源。咱们要使用这些资源,就要为这些表明资源的类型分配内存。在C#中,咱们通常使用new关键字来完成。访问资源包括如下几步:算法

    • 使用new操做符为类型分配内存(这个过程调用了IL指令newobj)
    • 初始化内存,设置资源的初始状态,来让这个资源可用(类型的实力构造器负责初始化类型状态)
    • 访问类型成员使用资源
    • 摧毁资源状态进行清理
    • 释放内存

  在C#中,咱们的操做时基于CLR来完成的,咱们全部对象都是从托管堆对来分配内存。当进程初始化(咱们的程序)时,CLR会画出一个地址空间区域来做为托管堆。同时,CLR会维护一个指针NewObjPtr,这个指针指向下一个对象在托管堆中分配的地址。当这个区域被非垃圾的对象填满后,CLR会分配更多的区域,这个过程将一直重复,直至整个进程的地址空间都被填满。32位进程的地址空间为1.5GB,64位进程为8TB。(这里顺便提一下值类型的生命周期,值类型对象分配在线程栈上,当离开做于域时,自动销毁。)数组

  C# new 操做符,会让CLR执行如下步骤:网络

    • 计算类型字段的所需字节数(这里的字段是全部字段,包括基类继承的)
    • 添加建立对象的额外所需字节数 (每个对象被初始化时,会建立类型对象指针和同步块索引,32位程序为8字节,64位程序16字节)
    • CLR检查区域中空间是否足够,假如足够,在NewObjPtr指针位置放入对象。这时候,对象分配的字节会被清零,而后调用类型的构造器(计算字节,NewObjPtr指针会指向旧的位置加上这个字节的位置,为下一个对象分配空间时候的位置),new操做符返回对象的引用。(例如在托管堆已经由A,B的状况下新构建了一个C)

  关于GC数据结构

  当程序调用new操做符建立对象时候,假如没有足够的地址空间来分配该对象,CLR就会执行垃圾回收(调用GC)。CLR采用引用跟踪算法,这种算法只关心引用类型的变量,避免了类型循环致使对象不能被回收的问题。引用类型包括类的静态和实例字段,方法的局部变量和参数。全部的引用类型被称之为根。编辑器

  CLR开始执行GC时候,会暂停进程中全部线程(防止CLR执行检查期间对象的状态被更改);而后CLR遍历托管堆中的全部对象,将同步块索引中字段的一位设置为0(标识全部对象都应该删除),而后CLR检查全部活动根,这些根引用了哪些对象。ide

  任何一个根引用了堆的对象,CLR会将对象的同步块索引的位设置位1。而后再检查对象中的跟,标记它们引用的对象。假如在遍历过程当中发现对象被标记,就跳过这个对象,再也不从新检查这个对象的字段,这样避免了循环引用。函数

  应用程序中全部的活动跟都检查完毕之后,这时候堆中的对象要么被标记了(称之为可达,由活动跟在引用),要么没有被标记(相应称为不可达)。CLR将不可达的对象内存回收。将可达对象进行内存整理,使对象内存在托管堆中是连续的(压缩过程当中CLR要从每一个根减去所引用对象在堆中的偏移字节数),以下图所示:性能

二、GC高效的处理方式—代

  CLR的垃圾回收基于代。测试

  同时GC回收垃圾时,作出了下面几点假设(能够先记下,从下文中体会).net

  • 对象越新,生存期越短
  • 对象越来,生存期越长
  • 回收堆一部份内存,比回收整个堆要快   

 

  解释下代:托管进程中有两种内存堆,分别是本机堆托管堆,CLR在托管堆上面为.net 的全部对象分配内存(托管堆又称为GC堆)。托管堆又分为两种,小对象堆大对象堆(LOH),小对象堆用来分配经常使用的资源对象内存(如类,数组等等),小对象堆的内存段进一步划分为3代,0代,1代,2代。(大对象堆用来分配一些大对象和非托管资源,咱们后文中专门来解释)

 

   托管堆初始化时,不包含任何对象,当咱们声明一个对象时,这个对象称为第0代对象。也就是说,第0代对象就是那些新构造的对象,并且垃圾回收器没有检查过的对象。例以下图中,托管堆中分配了A,B,C,D,E5个对象,它们就是第0代对象。

  接下来随着咱们不停地分配对象,第0代的堆内存使用完毕,且这随着程序的流转,C和E变得不可达,当咱们分配下一个内存F时,CLR就会执行一次垃圾回收。此时,C,E对象内存被回收掉,个人的ABD对象从第0代对象变为第1代对象。这时候,垃圾回收结束,第0代不包含任何对象。以下图所示:

  接下来随着程序的运行,又在0代中分配了对象F G H I J K,1代对象中B变得不可达。接下来给对象L分配内存时内存不足,将执行垃圾回收。CLR会为第0代对象和第 1代对象选择预算,因为第一代中的占用内存远少于预算,因此垃圾回收期只检查第0代的对象(基越新的对象得到越短),由于第0代对象包含更多的垃圾可能性更大,能够回收更多的内存。忽略了第一代中的对象,因此加快了垃圾回收速度。

 

 

  随着垃圾回收的不断进行,第1代的内存将不断增长,当第1代对象的内存增加到占用了占用了所有预算(0代给新对象分配内存就要进行GC),此时,会进行第1代的垃圾回收,幸存下来的对象被分配的第2代中去。托管堆只支持3代(0,1,2)。超过85000字节的对象称之为大对象,直接由第2代分配内存。

  代给GC带来的性能提高主要体如今没必要遍历托管堆中的每个对象。若是根或者对象引用了老一代的某个对象,垃圾回收期就能够忽略老对象内部全部引用(CLR的特征,引用跟踪算法,同步索引块中的一位标识),在更短的时间内构造好可达对象图。假如老对象字段引用了新对象,则由JIT编辑器内部的一个机制(单独解释)让垃圾回收期跳过。微软官方性能测试,0代执行一次GC,花费时间很多过1毫秒。

  • JIT的机制是在对象引用字段发生变化时候,设置一个对应位标志。这样,下一次GC回收资源内存时候,会知道上一次GC事后,哪些老对象被写入位标志,这样,只有位标志发生变化(也就是老对象字段发生变化)时候,才检查老对象是否引用第0代对象。

三、特殊类型的清理

  特殊类型:大多数对象只要分配内存就可使用。可是,还有部分对象须要分配本机资源(例如文件,网络链接,套接字,互斥体),咱们称这部分对象为特殊类型的资源。

  特殊类型的回收过程和特色:包含本机资源的类型被GC时,GC在回收内存以前,须要将本机资源终结(Finalization)。当CLR断定一个特殊类型的对象不可达时,对象将终结本身,释放包裹的本机资源,而后由GC回收其内存。

  Object基类型定义了虚方法Finalize,GC断定对象时垃圾后,调用对象的Finalize方法,这个方法通常以析构函数的形式出现。(ILSpy 反编译后的析构函数代码为protected override Finalize)。

   特殊类型注意事项:

    1. Finalize执行在GC以后,因此特殊类型的对象不是立刻被GC回收,由于Finalize方法可能要访问对象字段。这可能使对象提高到另外一级别的代,增长内存耗用。因此,尽可能避免引用类型的字段定义为可终结对象。
    2. Finalize方法执行时无顺序的。因此不要在Finalize方法中访问定义了其余Finalize方法的类型,由于另外一个类型对象可能已被终结。
    3. CLR用一个特殊的、高优先级专用线程调用Finalize方法避免死锁。
    4. 自定义包含了本机资源的托管类型时要继承自SafeHandle(派生自它保证本机资源在GC时被释放)。

  控制包装了本机资源类型对象的生存期:

      例如这里咱们要往D盘的1.txt中写入一部分文本,而后写完后想把这个文件删除,此时就会报 “System.IO.IOException:“文件“d:\1.txt”正由另外一进程使用,所以该进程没法访问此文件。”这样一个异常,这是由于本机资源未被释放(Finalize)。假如咱们想控制包装本机资源的类型对象的生命周期,就要实现IDispose接口。(若是类型对象的其中一个字段实现了这个接口,那么这个类型也就实现了Dispose模式。)而后咱们修改咱们的代码,成功删除文件。

   终结的内部实现原理:

    包装了本机资源的对象被回收时,会调用Finalize方法。

    包装了本机资源的对象建立的时候(定义了Finalize方法),在从堆中分配内存前,会将这个对象的指针添加到一个终结列表(由GC控制的内部数据结构)中。这个列表中的每一项,都指向一个定义了Finalize方法的对象,回收这些对象内存以前应该先调用它的Finalize方法(这里注意,虽然Object也定义了Finalize方法,可是CLR会忽略它,只有重写了Finalize方法的类型对象才会加入到终结列表)。以下图所示,C,E,F,I,J是定义了Finalize方法的类型对象,指向它们的指针被加入到终结列表中:

    

    垃圾回收开始进行,B,E,G,H,I,J被断定为垃圾,这时候垃圾回收器会扫描终结列表来查找这些对象的引用(这里找到了E,I,J),而后把这些引用从终结列表中移除,附加到freachable队列(也是GC的一个内部数据结构)。在freachable队列中的每个引用都表明即将进行Finalize调用的对象。经历过一轮GC后,堆内存以下所示:

    

    CLR使用一个高优先级的,专用的线程来调用Finalize方法,这个线程避免潜在的线程同步问题。当freachable队列为空时候,这个线程将休眠,freachable队列出现记录项,将唤醒这个线程。这样来看,包装了本机资源的托管对象至少要进行两次GC才能回收它的内存,第一次由专用线程来执行Finalize方法,第二次才由GC回收这个对象的内存(大于2次是由于这些对象可能被提升到老的一代)。

四、手动监控和控制对象生命周期

  CLR为每个AppDomain都提供了一个GC Handle table,容许程序监视或者控制对象的生命周期。这个表中的每一条记录项都包含托管堆中一个对象的引用监视控制对象标志。这里注意一个类GCHandle和一个枚举对象 GCHandleType。

  GCHandle调用Alloc方法时候,会扫描AppDomain的GC Handle table,查找一个可用的记录项存储对象的生命周期而且传回给对象引用。GChandle的Target属性,返回句柄表示的对象,以下图所示:

  GC发生时候会使用GC Handle table,首先,GC将全部对象标识为将要回收,扫描GC Handle table,全部GCHandleType为Normal和Pinned对象标识为根;而后查找GCHandleType为Weak的项,若是引用了未标记的对象,那么这个对象就是垃圾,且把这个项赋值为null;GC继续扫描中介列表,将无引用标识对象的引用放入freachable队列;GC再扫描GC Handle table,查找GCHandleType 为WeakTrackResurrection的记录想,这些记录想引用了未标记的对象(freachable队列中)变为垃圾,这些记录项赋值为Null。最后GC对内存进行压缩。

相关文章
相关标签/搜索