https://blog.csdn.net/jinzhuojun/article/details/9292467
Mono在2.10版本之前集成BOEHM内存管理,2.11及更高版本使用SGen内存管理
Unity中Mono版本一直停留在2.10前。
1、
1.1 BOEHM GC使用的是Mark-Sweep算法。
BOEHM GC基本步骤:
(1)标记阶段:从根节点遍历所有被引用的对象并标记;
(2)清除阶段:将所有未标记对象内存释放。
BOEHM GC是一种保守式的GC算法,只需要截断原来的malloc()和free()接口替换为自己的GC_malloc()接口。
1.2 (以下所有计算均按64位操作系统处理)BOEHM内存分配根据内存大小分为小对象分配逻辑和大对象分配逻辑,如果申请小于等于2048(2^12 / 2)字节则执行小对象分配逻辑,否则执行大对象分配。小对象分配会包含三种内存分配:普通内存、指针内存、非回收内存。GC_obj_kinds数组用来区分三种不同类型的内存,所以该数组长度为3。每一种类型都维护一个空闲内存链表数组ok_freelist,ok_freelist是一个长度为MAXOBJGRANULES(MAXOBJGRANULES为128)+1的数组,每个数组元素指向一个空闲链表,该链表每一个元素指向一个大小为index*16的空闲内存块。其中index为该链表所在数组的索引,16为最小内存分配粒度,128*16=2048即为小对象最大分配上限。
小对象内存分配,首先会查找GC_size_map表找到内存大小对应的内存分配索引。其中GC_size_map是一个长度为MAXOBJBYTES(MAXOBJBYTES为2048即最大分配内存上限)+1的数组,该数组的key代表申请内存大小,value即为内存分配索引。然后根据索引查找对应类型的GC_objfreelist,如果存在空闲内存块则直接返回对应指针并将该内存块从空闲链表移除。否则执行一次GC,如果GC后还没有空闲列表则需要分配新的空闲内存块,分配内存时,首先根据申请内存大小通过公式(bytes+PAGE_SIZE-1)/PAGE_SIZE得到一个index,从index开始遍历GC_hblkfreelist数组,直到找到一个空闲块。(其中GC_hblkfreelist是一个类似于ok_freelist的数组,长度为N_HBLK_FLS(N_HBLK_FLS为28)+1,每个元素也存储一个空闲内存链表,每个链表元素指向一个PAGE_SIZE*index大小的内存块,其中PAGE_SIZE为4096,index即为所在数组索引。)然后将该空闲块拆成一个PAGE块和另一个块,另一个块放回GC_hblkfreelist中,PAGE_SIZE块根据申请大小拆成空闲块列表返回给ok_freelist。
如果是大对象内存分配,直接查找GC_hblkfreelist分配一个内存块。
每个PAGE块分配时会生成一个HBLKHDR结构,该结构会记录hb_sz、 hb_marks、descr等信息,其中h b_sz为拆分成ok_freelist块的大小,hb_marks标记拆分块使用状态。HBLKHDR结构用一个二维数组记录,每个HBLKHDR的存储位置为[p>>22][p>>12 & 1024],其中P代表PAGE块起始地址。HBLKHDR由非回收内存分配。
2、SGen(Simple Generational GC)是一种分代式垃圾回收器
SGen特点:
(1)分两代:the nursery generation、the old generation;
(2)Stopiing the world:垃圾收集时,所有运行线程需要停止;
(3)两种主要收集方式:
minor collection:针对nursery generation,采用copying collection;
major collection:针对the old generation,使用mark-sweep方式。
(4)Write barriers
为了减少GC开销,有时可能只需要进行nursery collection而不是完整的回收,但可能存在old generation中对象指向nursery中对象的情况:
这种情况就需要write barriers解决,Mono中通过一个cardtables实现write barriers。Cardtable会把major heap分为固定大小的块(SGen中为512bytes)称为card,每个card中有一个字节用来标记该card是否有对nursery中对象的引用,如果有的话,就会扫描该card中所有块,找到引用的位于nursery中的对象进行标记Copy。
(5)多线程
标记阶段可以多个线程并行标记,每一个线程包含一部分根对象,每一个线程有自己的gray stack,当多个线程同时遍历到同一个引用对象时,只有第一个找到该引用对象的线程才会将该对象压入自己的gray stack进行后续处理。
2.1 minor collection和major collection:
(1)minor collection 主要针对nursery generation。步骤如下:
a、识别并且标记 Pinned Objects
Pinned Object分为两种:
第一种是使用fixed修饰符指定或者通过P/Invoke传递给非托管代码使用的对象引用,这种对象创建时就会标记为PINNED;
第二种就是通过保守式扫描栈和线程寄存器,找到可能引用的对象也标记为PINNED。
每个Mono Object创建时都会创建一个vtable指针,vtable的低三位标记对象的状态,如图2-1所示:

b、遍历扫描所有roots找到所有引用,所有可达对象会被拷贝到the old generation并被标记为FORWARDED。整个过程使用一个gray stack实现,不断将可达对象压入栈中,然后弹出栈中对象遍历其引用对象再压入栈,直到栈为空,所有引用对象处理完毕。


c、清除未标记和非pinnded Object,如果是小对象会将空闲空间放到一个freelist供下次分配。
(2)major collection 主要针对the old generation,使用mark-sweep方式实现,标记阶段和minor collection一样,也是从root遍历查找标记所有引用对象,sweep阶段会将未标记对象释放,然后统计每个block中slot的占用率,如果指定slot大小的占用率低于设置的阈值时,会将该指定大小的slot顺序拷贝到一个新分配的blocks中,并将原slot置空等待下次回收并且重新构建free lists。
另外Mono对major collection的sweep阶段进行了优化即concurrent sweep,sweep阶段做的工作就是遍历blocks,重置标记位,置空垃圾对象占用的slot以及重新构建free lists,这些操作都不会影响minor collection,所以minor collection可以和concurrent sweep并行进行。
2.2 内存分配
(1)小对象内存分配
Sgen初始化默认会分配一个4MB的连续空间作为nursery generation空间,当小于等于8KB的对象申请空间时,会在the nursery generation中查找可用空间,如果已满不足分配则会触发minor collection。如果在多线程环境下,每个线程会分配一小块nursery,Mono把它称为TLABs(thread local allocation buffers),目前TLABs固定大小为4KB。
the old generation 会分配多个固定大小为16KB的Block,每个Block分割成等大小的slot,所有的slot记录在一个free lists中便于分配。
(2)大于8KB的对象,Sgen会视为大对象,直接从操作系统申请内存空间,使用完后直接归还给操作系统。
网友评论