解读MONO内存管理和回收

简介

Mono支持内存自动回收,因为MONO集成了内存回收算法。在1.X到2.X的版本中,MONO集成了贝母内存管理及回收算法;而在3.X或更高版本中,则开始启用SGEN内存管理及回收算法。

本周研究了2.6.X版本的BOEHM(贝母)的内存管理及GC算法部分代码。BOEHM属于一个开源项目,其实现为支持C/C的内存管理及GC,在C/C项目中,将分配内存部分接口(malloc或者其他分配内存等接口)替换成BOEHM提供的内存分配接口(GC_malloc),则可以在C/C++项目中实现内存自动管理,无需手动调用free等释放内存接口。而MONO正是基于BOEHM,实现了内存管理及自动GC。

 一、GC实现的方式 

1. 引用计数算法:引用技术算法是唯一一种不用用到根集概念的GC算法。其基本思路是为每个对象加一个计数器,计数器记录的是所有指向该对象的引用数量。每次有一个新的引用指向这个对象时,计数器加一;反之,如果指向该对象的引用被置空或指向其它对象,则计数器减一。当计数器的值为0时,则自动删除这个对象。

2. Mark&Sweep算法:也叫标记清除算法,标记阶段通过标记所有根节点可达的对象,未被标记的对象则表示无引用,可回收,BOEHM正是使用该算法实现内存自动回收; 节点复制算法:将活的节点复制到新内存区,老内存区一次性释放,对象会被转移,应该还需要设置元数据中间层,具体实现未做研究;

 二、BOEHM算法内存管理 

BOEHM算法采用标记清除法,在标记阶段通过访问根节点,并遍历到叶子节点,最终将所有存在引用的内存都标记出来,而未标记的内存(所有从堆中分配的内存BOEHM中均有记录,因此可将未标记的部分清除释放掉)。

2.1 下面介绍BOEHM分配内存过程,在介绍分配流程之前,先介绍关键数据结构。

2.1.1 GC_size_map[2049]

一个MAXOBJBYTES(2048+1)个元素的数组,每个数组元素为一个整数值,分配该内存为了在分配内存时快速定位出分配的粒度的倍数(其实完全可以用(bytes+16-1)/16来规避掉这个数组,这么做应该是为了减少计算量…),而16这个是BOEHM分配内存的最小粒度,也就是说分配1字节,BOEHM也会给你分配16字节的内存(我这里看代码时是根据64位机器定义的宏来分析的,不同平台字节略有差异,不影响分析)。

同时若分配的内存大小大于2048时,此时要执行另一个大内存分配分支(由于流程大同小异,因此对大内存分配暂时不做研究,下述介绍均为对2048个字节内的内存分配及回收介绍)。

2.1.2 GC_obj_kinds[3]

三个元素的数组,BOEHM定义这个数组是为了区分分配的内存类型,在BOEHM中,存在存储指针内存,普通内存和非回收内存之分,本次的分析为普通内存分配的流程(因为这是应用程序调用最为频繁,而非回收内存属于BOEHM自己为了做到内存管理而分配的内存,这些内存不需要标记和回收的),每个元素包含一个ok_freelist[MAXOBJGRANULES+1]的数组,MAXOBJGRANULES为128,为128的原因是,每个GRANULES是一个分配粒度,即16字节,那么128*16=2048,刚好是能分配的小内存的上限。

2.1.3 ok_freelist这个数组每个元素为一个空闲链表的指针,指向的内存块大小是index * 16,即第一个元素存储16个字节大小块的内存块链表指针,而第128个元素存储的链表中内存块大小为128*16=2048的内存块大小的指针。

这个数组保存的内存块大小作用为:当分配内存时,会优先检查该链表是否存在FREE块,若存在,则直接可以在链表中取出一块返回给应用上层,若否,则会再调用分配函数,分配较大一块内存,然后将大内存分割为小内存链表存储在ok_freelist中(具体存储INDEX由上层分配的内存大小需要几个GRANULE决定)。

2.1.4 GC_hblkfreelist[N_HBLK_FLS+1]

数组N_HBLK_FLS=28(为何是这个值是因为设计就是这样),GC_hblkfreelist每个元素存储的也是一个空闲内存链表,与ok_freelist不同的是,它的内存基本块大小是PAGE_SIZE,即4096,与ok_freelist类似,每个元素存储的链表块大小与index相关,可以这么理解,第一个元素的链表内存块大小为PAGE_SIZE,第二个为PAGE_SIZE*2,以此类推。由于本文只介绍小内存分配,因此不需要考虑特别大内存分配的处理方式。

当需要分配内存时,例如当请求分配16个字节的内存时(上层调用下来的已经进行粒度GRANULE对齐了),此时会将其进行(bytes+PAGE_SIZE-1)/PAGE_SIZE得到index,然后检测GC_hblkfreelist[index]是否存在空闲快,若无则会继续向后遍历,找到后,将大块拆分成小块,再重新将两个小块的后一块推入GC_hblkfreelist合适的位置,将前一块用来分配,由于上层只分配16字节内存,当给了上层PAGE_SIZE大小的内存,此时会将该内存按照16字节划分,并生成链表(链表指针无需额外增加内存,只用16字节的中的前8个字节作为指针即可),将链表存储到ok_freelist[1]中,此时上层就能完成内存分配。

GC_hblkfreelist中的内存是通过下述调用来分配

从上可以看出,一个内存分配过程,首先会检测ok_freelist链表,若存在FREE块,则直接取出返回(当然会设置使用标记,GC会使用),若ok_freelist中无对应大小的内存,则从GC_hblkfreelist调用获取PAGE_SIZE内存并存储,下次再分配就可以直接从ok_freelist中取得了,相当于做了一个简型内存池。

2.1.5 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。

其进行的函数调用堆栈为:

也会调用setup_header用来初始化对应的HBLKHDR,调用GC_install_header来分配第二级数组及分配HBLKHDR(这个结构是从非回收内存中分配的)。

上述描述可以看出,BOEHM分配的内存指针通过32BIT来描述,如果同时存在64BIT的内存和32BIT的内存低地址位相同,那么就会冲突,BOEHM通过其他方式规避该问题(通过指定分配内存的起始位置(这里没详细看,linux它是通过sbrk来分配内存,GDB也发现了这一点,由于不影响分析流程,所以对于该细节未做过多研究)。

发布了3 篇原创文章 · 获赞 3 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/jinangl/article/details/104747577