mono-memory-manager

mono内存管理

GC管理方式

1 引用计数

引用计数的方式就是开辟一个内存出来,标记,如果这个内存的被其他地方引用,则这块内存的引用计数+1,解除内存引用-1,等到计数为0则把这块内存回收,优点:内存回收快,基本上瞬发,缺点:容易出现循环引用,导致内存泄露

2 Mark&Sweep算法

也叫标记清除法,标记阶段会把所有的内存都标记成可回收,然后从内存root节点往后找,能到达的标记不可回收,剩下的全是可以回收的,实现这个算法主要有BOEHM(贝母)算法,Sgen算法。优点:可以回收干净,不用出现内存泄漏,缺点:内存回收不及时,内存回收一次时间较长

mono使用的管理方式

mono主要用的管理方式为Mark&Sweep算法,其中2.1版本前用的是BOEHM,往后使用的是Sgen算法,本文主要讨论BOEHM,下一次讨论Sgen

内存开辟:

*GC_size_map[2049] *

MAXOBJBYTES(2048+1)每个数组为一个整数值,分配该内存为了在分配内存时快速定位出分配的粒度的倍数(其实完全可以用(bytes+16-1)/16来规避掉这个数组,这么做应该是为了减少计算量…),而16这个是BOEHM分配内存的最小粒度,也就是说分配1字节,BOEHM也会给你分配16字节的内存

GC_obj_kinds[3]

这个主要是三个数组,三个数组里面存放的是一个内存数组,为啥用三个,主要是内存分成三部分,指针内存,普通内存,不删除内存,本文分析普通内存,指针内存主要存放的是指针,不删除一般是BOEHM自己的内存。

每个元素包含了一个ok_freelist[MAXOBJGRANULES+1]数组,MAXOBJGRANULES为128,为128的原因是,每个GRANULES是一个分配粒度,即16字节,那么12816=2048,刚好是能分配的小内存的上限

ok_freelist这个数组是为每个粒度的内存分配了一个链表,例如第一个分配的是16字节内存的链表指针,第二个是32字节链表指针,以此类推,而第128个元素存储的链表中内存块大小为128*16=2048的内存块大小的指针。

这个数组的主要存在原因是,当需要申请内存时,检查ok_freelist的内存,找到当前粒度的,查看是否能找到空闲内存,找到了,标记,返回给应用层,否则再次分配内存,分配这个内存一般比较大,然后进行分割,把这个内存存进ok_freelist里面,下次可以直接使用

GC_hblkfreelist[N_HBLK_FLS+1]

数组N_HBLK_FLS=28(为何是这个值是因为设计就是这样),GC_hblkfreelist每个元素存储的也是一个空闲内存链表,与ok_freelist不同,这个内存存放的大小基数时PAGE_SIZE,即4096,跟ok_freelist一样,第一个块是PAGE_SIZE大小,第二个PAGE_SIZE*2, 以此类推,这样的一个大内存存在方式

分配时,根据PAGE_SIZE来计算出index,然后检查index是否存在内存块如果没有找到,则继续往后遍历,找不到系统分配一个内存块,然后根据这个内存块根据使用的粒度进行切割,然后把其分成两小块,一块放入hblkfreelist内,前一块用来分配,生成链表把链表存入到ok_freelist内。

内存的开辟方式:先把申请的内存进行格式化,先查找ok_freelist是否存在当前从取出标记使用,返回给应用层,否则从GC_hblkfreelistPAGE_SZIE分配大内存,存储到ok_freelist中,下次可以直接使用,相当于内存池。

Hblkhdr数据结构

此数据结构为关键数据结构,GC能否正常运行全靠这个块信息描述。当从GC_hblkfreelist分配PAGE_SIZE的一块内存时,会生成一个hblkhdr的对象,此对象描述该PAGE_SIZE内存块,该hblkhdr会存储(hb_sz, hb_mark5,descr等信息),hb_sz存储上层分配的传递的内存块大小,当分配16字节时,hb_sz就会被设置为16。

设置这个元素的作用就是为了知晓存储在该PAGE中的元素的大小(若分配16字节,说明上层分配内存存储的对象最大为16字节),而hb_mark用来存储该PAGE哪些块被使用(每个BIT可以描述一个小块,每个小块最小为16字节,因此5个8字节有足够的BIT位来描述一个PAGE,因为可以描述588*16个字节,超过了4096个字节)。

分配好HBLKHDR结构后,它会被存储到二级数组中,存储方式为(这里假定PAGE的起始位置为P指针) top_index[p>>22]->bottom_index[p>>12 & 1024]的位置,12是因为每个PAGE为4096字节,即2的12次方。即会把每个PAGE的地址的高10位作为索引,中10位作为索引,在二级数组中存储该HBLKHDR(当然,这个二级数组并非一启动就生成这么大的二级数组,而是运行过程中生成(否则过于浪费内存,如果一开始就生成,则至少需要102410248,即8M内存,而很多巢位在运行期间根本不会被用到)),

生成了这个数据后,任何一个指针都可以找到它属于的HBLKHDR的描述符,而根据该描述符,可以知晓该指针指向的对象最大的内存块大小以及它的标记情况(hb_sz和hb_mark)。

介绍完上述之后,大概可以有一个思路,分配内存的大体流程为:先检查ok_freelist—->GC_hblkfreelist—>分配一个PAGE—>并分配描述HBLKHDR。

BOEHM GC原理

根据Hblkhdr数据结构,可以从其得知当前的OBJ大小,然后根据内存对齐原理,可以得到当前的内存存放位置,经历几层遍历就能找到引用树

具体实现,先把其他线程挂起(不挂起可能还在分配内存)然后从内存root,静态区,堆栈,通过二级数组找到HBLKHDR,然后检查PAGE_SIZE大小是否时空闲状态,空闲状态则无需理会,没有被引用。若是,则标记该指针指向的OBJ为使用状态,并将对应的标记位设置为可用(每给出一个指针,都能根据HBLKHDR知道该OBJ的大小,因为其中存储了hb_sz字段),然后再遍历该OBJ,找出可能的指针,并一一标记。在整个标记过程开始前,GC会清除所有HBLKHDR的标记字段(此时其他线程已暂停),所有的HBLKHDR有一个队列头,遍历队列能获取到所有的HBLKHDR。然后标记完成后,被标记的说明存在引用,未被标记的则会被释放到ok_freelist,若整个HBLKHDR所有标记都未被设置,则会将HBLKHDR还回到GC_hblkfreelist中,并且会根据其地址,将相邻的PAGE合并成大PAGE再调整其在GC_hblkfreelist中的存储位置

总结

mono的内存对齐方式采取基数为16,每分配一个内存时,都会尝试产生ok_freelist列表的小内存,例如16,32,48等等,会同时存在三份ok_freelist数组链表,内存最小采取16是因为如果产生太多小内存,寻找内存起来则会出现特别的慢,细碎。每个ok_freelist都会存在一个基数为PAGE_SIZE大小的对象分配区

打赏

扫一扫,分享到微信

微信分享二维码

请我喝杯咖啡吧~

支付宝
微信