浅谈 CLR GC & Boehm GC

涉及CLR、Boehm、Incremental、IL2CPP

Posted by SWZ on June 10, 2020

前言

GC 全称 Garbage Collection。也叫垃圾回收。

引用计数算法:每一个对象都有一个引用计数器,当一个对象被引用时候,计数器就会+1,当不被引用时候,计数器就-1,当所有的引用都没有时候,那么计数器就会归0,那么这个对象就会被认为不再使用了,会回收此对象。

根搜索:根搜索就是从一个根开始遍历所有的叶子,然后把所有不需要的叶子清除。此中又可以分出1.Mark-Sweep 2.Copying 3.Mark-Compact 。这几种区分无非是在对内存进行回收后如何处理内存分布的一些策略。


CLR GC

首先,要说明的是CLR选择的是引用跟踪算法,它是基于可达性(reachable)分析,从出发找到其引用的对象,再到其间接引用的对象,形成一条引用链,凡是不在引用链里面的对象,被称为不可达(Unreachable)对象,里面也会涉及的Mark-Compact + 分代。这种算法比起引用计数算法的好处就是,解决了循环引用问题。例如一个父物体引用了一个子物体,而这个子物体也引用了该父物体,这种引用关系会阻止两个对象的计数器达到0。

运行原理

引用跟踪算法只关心引用类型的变量,因为只有引用类型的变量才能引用堆上的对象。我们将所有引用类型的变量称之为

  • 开始GC,暂停进程中所有的线程,防止线程在CLR检查期间访问对象并更改其状态。
  • CLR进入GC标记阶段,CLR会遍历堆中所有的对象,将同步块索引中的位设为0,这表明所有对象都应该删除。
  • 然后CLR检查所有活动根,查看他们引用了哪些对象。任何根如果引用了堆上的对象,CLR就会标记那个对象,将同步块索引中的位设为1。检查完毕后,堆中的对象要么已标记,要么未标记。已标记对象不能被回收,未标记对象是不可达的。
  • 进入GC压缩阶段(compact,更接近碎片整理的意思),此时CLR已经知道哪些对象需要被回收。CLR对堆中已标记的对象进行压缩,使他们占用连续的内存空间,解决了堆的空间碎片化的问题。
  • 由于幸存者对象的根现在还是最初内存中的位置,而不是移动后的位置,在暂停线程恢复执行的时候依旧会访问旧的内存位置,所以压缩阶段CLR会从根减去所引用的对象在内存中偏移的字节数。这样就能保证每个根还是引用和之前一样的对象。

CLR的GC是基于代的垃圾回收器,并且对代码做出了以下几点的假设:

  • 对象越新,生存期越短。

  • 对象越老,生存期越长。

  • 回收堆的一部分,速度快于回收整个堆。

代的分配过程

堆在初始化时不包含任何对象,添加到堆内的新对象称之为第0代对象。

CLR初始化时会为每一代选择一个预算容量(xx KB),如果分配一个新的对象发现已经超过预算则会触发一次GC,释放掉不可达的对象,并对内存块进行压缩,使它们相邻紧凑。

经过一次垃圾回收后,第0代就不包含任何对象了,原第0代的幸存者被提升至第1代。注意,每一次的垃圾回收时CLR都会先看第1代使用的内存是否小于预算来决定是否要对第1代也进行检查,这样如果第1代仅仅占用了少部分内存的话就可以跳过检查,加快垃圾回收速度。

托管堆只支持三代,CLR初始化时会为每一代选择预算,当然期间这个预算也会自调节。比如发现第0代存活下来的对象很少,就会减小第0代的预算,意味着垃圾回收将会变得更加频繁,但每次做的事情也变少了。

GC触发条件

  1. 第0代超过预算(最常见的触发条件)
  2. CLR内部使用函数MemoryResourceNotification监视系统内存总体使用情况发现低内存
  3. 显示调用GC.Collect

    GCCollectionMode有三种模式:

    Default(默认Forced) Forced(强制回收指定代以及低于 它的所有代) Optimized(只有能释放大量内存或者能减少碎片化的前提下才执行回收)

  4. 卸载AppDomain
  5. CLR关闭

Boehm GC

Boehm GC用的是Mark-Sweep算法(标记清除),是非分代(non-generational)和非压缩(non-compacting)的。由于它是非分代的,必须遍历整个内存,并且随着内存的增长,它的性能就会降低。非压缩由会有内存碎片化的问题,可能会导致之后生成的对象都没法放进这些间隙中,又去申请内存,导致内存不断上升。

运行原理

在标记阶段通过访问根节点,并遍历到叶子节点,最终将所有存在的内存都标记出来,其余未标记的部分可清除释放掉。


Incremental GC

新版本Unity的实装GC,解决主线程卡顿问题。由于进行一次GC主线程会被迫停止,遍历所有节点,决定哪些可以被GC掉,这些操作会有个明显的峰值产生,卡顿非常明显。

运行原理

Incremental GC会把之前暂停的主线程的事情分摊到10帧里面执行,一帧一帧分析哪些东西需要被GC,这时主线程就不会一个峰值,他把这个峰值进行平摊,消耗的总体时间可能没有改变,但是可以改善对主线程卡顿的影响。


IL2CPP GC

GC的机制Unity进行了重写,可以说是个升级版的Boehm。