作者:字节游戏中台客户端团队 - 潘风
引言
Unity3D引擎在管理托管堆内存时,没有直接使用操作系统自带的内存分配函数例如malloc和free来直接分配和释放每一个对象,而是采用一套内存分配GC算法来管理托管堆上所有内存对象的分配和回收。在Unity的1.X到2.X的版本中,基于开源的C++BoehmGC算法,而在3.X或更高版本中,开始启用SGEN算法。采用il2Cpp生成的Unity工程中,使用的是BoehmGC算法。
BoehmGC算法思路
BoehmGC是一种mark-sweep(标记-清扫)算法,大致思路和Java的GC算法类似,主要包含4个阶段:
-
准备阶段:每个托管堆内存对象在创建出来的时候都有一个关联的标记位,来表示当前对象是否被引用,在标记之前,该标记位是0。
-
标记阶段(Mark phase):从根部内存节点(静态变量、栈、寄存器)出发,遍历扫描托管堆的内存节点,将被引用的内存节点标记为1。
-
清扫阶段(Sweep phase):遍历所有节点,将没有被标记的节点的内存数据清空,并且基于一定条件释放。
-
结束阶段(Finalization phase):触发GC_register_finalizer等注册的回调逻辑。
BoehmGC算法特点
BoehmGC是一个保守式GC算法,与之对应的是准确式GC。所谓保守式GC算法就是其无法分辨一个内存值是一个指针还是非指针,所以会对所有内存当做一个指针做尝试,所以无法使用Copying算法。而准确式GC就是GC可以直接分辨出这是个指针还是非指针。而且可以使用Copying算法优化内存的使用。
本篇主要介绍BoehmGC算法关于内存分配的实现逻辑,下一篇主要介绍该算法关于垃圾回收的实现逻辑。
内存分配流程
内存类型
首先GC_malloc是BoehmGC中用来替换malloc()函数的API,接收一个参数lb,即需要分配的内存大小。 GC_malloc_kind(size_t lb, int k) 接收2个参数,包括需要分配的内存大小,以及类型。
void * GC_malloc(size_t lb)
{
return GC_malloc_kind(lb, NORMAL);
}
void * GC_malloc_kind(size_t lb, int k)
{
return GC_malloc_kind_global(lb, k);
}
首先这里的参数k,用来区分不同的分配内存类型,k的情况可以有三种类型,分别是PTRFREE,NORMAL,UNCOLLECTABLE等。说明如下:
-
NORMAL:无类型的内存分配,对于GC而言,因为无法得到对象的类型元数据,所以在做GC时会按照指针对齐的方式扫描该内存块,只要发现类似通过指针校验的地址,都会认为该对象引用了该指针地址指向的对象。
-
PTRFREE:无指针内存分配,明确告知GC,该对象内无任何指针信息,即在GC时无需查找该对象内是否引用其他对象。在mono/il2cpp中的int型的数组,byte型的数组,字符串等使用该类型的内存分配。
-
UNCOLLECTABLE:属于BOEHM自己为了辅助实现内存管理而分配的内存,这些内存不需要标记和回收的。
大内存or小内存
首先判断是否大于2048字节,如果小于2048字节,则有一套分配小型内存的流程来处理,否则走大内存分配的流程。
void * GC_malloc_kind_global(size_t lb, int k)
{
if (SMALL_OBJ(lb)) {
//小内存分配流程
...
} else {
//大内存分配流程
}
}
小对象内存分配
尺寸对齐-GRANULE
针对小内存分配,并不是直接分配实际的内存大小,而是分配16字节倍数的内存,保证按照指针对齐的方式管理内存。因此,第一步需要计算出实际需要分配的字节大小。实现思路是,提出一个“粒度(GRANULES)”的概念,即一个GRANULE的大小是16字节。实际分配内存的时候按照GRANULE为基本单位来分配,例如分配2个GRANULE,即分配32字节。分配过程中,按照原始需要的大小,计算并映射得到实际需要分配的GRANULE个数,代码如下:
//lb是原始的分配大小,lg是GRANULE(1~128)。
size_t lg = GC_size_map[lb];
例如需要18字节的内存,则lg=2,即实际分配2个GRANULE(32字节),如果需要1字节的内存,则lg=1,即实际分配1个GRANULE(16字节)。
GC_size_map[] 是一个 “尺寸表”, 维护lb->lg的映射关系。GC_size_map最多可以返回128个GRANULE,即小内存的大小上限(128 * 16 = 2048)。GC_size_map数组本身通过懒加载的方式不断扩充。下图反应GRANULE和内存大小的对应关系:
空闲链表-ok_freeList
决定了GRANULE的大小之后,首先会从ok_freelist链表中查看对应的空闲内存块,如果发现有,则直接返回这块内存,完成分配。算法维护了一个数据结构obj_kind,如下:
struct obj_kind {
void **ok_freelist;
struct hblk **ok_reclaim_list;
...
} GC_obj_kinds[3];
GC_obj_kinds[3]对应上文提到的3种内存类型,分别是PTRFREE,NORMAL,UNCOLLECTABLE,即每种类型都有一个obj_kind结构体信息。
每一个obj_kind结构体都维护了一个ok_freelist二维指针链表,用来存放空闲的内存块。ok_freelist维护128个链表,即链表0~链表127。其中,链表N中的每个内存块的大小是(N+1) * 16,即这里的N等价于之前的GRANULE个数。ok_freelist的数据结构如图:
首先,根据计算得到的GRANULE=N个数,查找对应大小的链表ok_freelist[N],如果存在且元素个数大于0,取出ok_freelist[N]中第一个内存块地址ok_freelist[N][0]返回,并且将其从链表中移除。操作过程如图:
对应的逻辑如下:
GC_INNER void * GC_generic_malloc_inner(size_t lb, int k)
{
struct obj_kind * kind = GC_obj_kinds + k;
size_t lg = GC_size_map[lb];
void ** opp = &(kind -> ok_freelist[lg]);
...
lg = GC_size_map[lb];
//查看freelist的内存块
opp = &(kind -> ok_freelist[lg]);
op = *opp;
//未找到相应的内存块
if (0 == op) {
...
//从底层内存块链表中查找或创建
op = GC_allocobj(lg, k);
if (0 == op)
returnNULL;
}
}
//将找到的内存块从空闲链表中移除
*opp = obj_link(op);
obj_link(op) = 0;
...
//返回内存块地址
return op;
}
ok_freelist链表最初为空,即一开始ok_freelist可用的空闲内存块。如果ok_freelist中没有相应的空闲内存块,则调用GC_allocobj(lg, k)去底层查找可用的内存,代码逻辑如下:
GC_INNER ptr_t GC_allocobj(size_t gran, int kind)
{
void ** flh = &(GC_obj_kinds[kind].ok_freelist[gran]);
while (*flh == 0) {
ENTER_GC();
GC_collect_a_little_inner(1);
EXIT_GC();
if (*flh == 0) {
GC_new_hblk(gran, kind);
if (*flh == 0) {
ENTER_GC();
...
GC_collect_a_little_inner(1);
...
EXIT_GC();
}
}
}
return (ptr_t)(*flh);
}
GC_allocobj的核心逻辑是调用GC_new_hblk(gran, kind)去底层内存池获取内存,并且查看底层内存池中是否分配了空闲的内存块,如果没有则通过系统函数例如malloc分配内存给底层内存池,如果内存迟有,直接取出一块返回。GC_new_hblk的代码逻辑如下:
GC_INNER void GC_new_hblk(size_t gran, int kind)
{
struct hblk *h; /* the new heap block */
GC_bool clear = GC_obj_kinds[kind].ok_init;
/* Allocate a new heap block */
h = GC_allochblk(GRANULES_TO_BYTES(gran), kind, 0);
if (h == 0) return;
/* Build the free list */
GC_obj_kinds[kind].ok_freelist[gran] =
GC_build_fl(h, GRANULES_TO_WORDS(gran), clear,(ptr_t)GC_obj_kinds[kind].ok_freelist[gran]);
}
可以发现GC_new_hblk的主要逻辑有2步,其一,调用GC_allochblk方法进一步获取内存池中可用的内存块;其二,调用GC_build_fl方法,利用内存池中返回的内存块构建ok_freelist,供上层使用。
核心内存块链表-GC_hblkfreelist
底层内存池的实现逻辑类似ok_freelist,维护了一个空闲内存块链表的指针链表GC_hblkfreelist,和freeList不同的是,这个链表中的内存块的基本单位是4K(4096)字节,即一个page_size的大小。
GC_hblkfreelist共有60个元素,每个元素都是一个链表,例如,GC_hblkfreelist[N]是一个链表,链表中的每个内存块的大小是4K的倍数,但是链表[N]中的每个内存块不一定是等大小的,如下图:
内存块-hblk、头信息-hblkhdr
首先,链表中每一个内存块,以大小为4096(4KB)的内存块为基本单位,一个4096大小的内存块称为hblk,数据定义如下:
struct hblk {
char hb_body[HBLKSIZE]; //HBLKSIZE=4096
};
每个hblk都有相应的一个header信息,用于描述这个内存块的状况,数据的定义如下:
//头部信息
struct hblkhdr {
struct hblk * hb_next; //指向下一个hblk
struct hblk * hb_prev; //指向上一个hblk
struct hblk * hb_block; //对应的hblk
unsigned char hb_obj_kind; //kink类型
unsigned char hb_flags; //标记位
word hb_sz; //如果给上层使用,则表示实际分配的单位,如果空闲,则表示内存块的大小
word hb_descr;
size_t hb_n_marks;//标记位个数,用于GC
word hb_marks[MARK_BITS_SZ]; //标记为,用于GC
}
当每个内存块创建出来的时候,都会生成一个相应的header信息,并且header信息以某种hash算法存储在一个全局队列中,header信息是关键的数据结构,GC能否正常运行全靠这个块信息描述。header的存取逻辑将在后文中讲解。
hb_next表示hblkfreelist链表中下一个内存块,hb_block指向对应的内存块,hb_obj_kind表示内存块类型,hb_flags是标记位,用于记录一些信息。hb_sz表示这个内存块本身的大小。
内存块和header的信息如下图:
hblk内存块查找
了解了数据结构之后,查看GC_allochblk方法的代码,逻辑如下:
structh blk *GC_allochblk(size_t sz, int kind, unsigned flags/* IGNORE_OFF_PAGE or 0 */)
{
...
//1.计算需要的内存块大小
blocks_needed = OBJ_SZ_TO_BLOCKS_CHECKED(sz);
start_list = GC_hblk_fl_from_blocks(blocks_needed);
//2.查找精确的hblk内存块
result = GC_allochblk_nth(sz, kind, flags, start_list, FALSE);
if (0 != result) return result;
may_split = TRUE;
...
if (start_list < UNIQUE_THRESHOLD) {
++start_list;
}
//3.从更大的内存块链表中找
for (; start_list <= split_limit; ++start_list) {
result = GC_allochblk_nth(sz, kind, flags, start_list, may_split);
if (0 != result) break;
}
return result;
}
STATIC int GC_hblk_fl_from_blocks(word blocks_needed)
{
if (blocks_needed <= 32) return blocks_needed;
if (blocks_needed >= 256) return (256-32)/8+32;
return (blocks_needed-32)/8+32;
}
步骤如下:
-
首先根据上层需要分配的内存大小,计算需要的内存块大小,例如上层根据GC_size_map计算出,需要分配16字节,则sz=16,OBJ_SZ_TO_BLOCKS_CHECKED的计算公式是:
OBJ_SZ_TO_BLOCKS_CHECKED(lb) = divHBLKSZ(lb + HBLKSIZE - 1) = divHBLKSZ(lb + 4096 - 1) / 4096
结果是需要分配的HBLK内存块个数,因为GC_hblkfreelist链表是以4096的大小作为基本单位。例如小于4096字节,结果是1。对于小对象(小于2048字节),内存块个数是1。
-
GC_hblk_fl_from_blocks进一步处理,GC_hblk_fl_from_blocks是一个关键的方法,根据实际需要的内存块数(blocks_needed),判断并决定从哪一个GC_hblkfreelist链表查找,start_list是开始查找的链表index,即从GC_hblkfreelist[start_list]开始查找。因为并不是需要blocks,就一定会从GC_hblkfreelist[blocks]的链表中查找,而是遵循一定的转换规则,具体如下:
-
如果blocks_needed小于32,则startlist=blocks_needed,直接去GC_hblkfreelist[blocks_needed]中查找。
-
如果blocks_needed位于32~256,则startlist=(blocks_needed-32)/8+32,即blocks_needed每增加8个,对应GC_hblkfreelist[index]的index增加1。
-
如果blocks_needed大于256,则都从GC_hblkfreelist[60]链表中查找。
之所以采取这样规则,是因为链表的总个数有限,一共只有60个。与之相对应的,在创建内存块构建链表时,也会按照GC_hblk_fl_from_blocks的规则将对应大小的内存块加入相应级别的GC_hblkfreelist链表中。链表结构如下图:
左边的数字是链表的级别,可以看到从GC_hblkfreelist[32]开始,存储的内存块大小不等相差,例如GC_hblkfreelist[32]存储的内存块大小范围是128KB~156KB。
-
-
决定从哪个链表开始查找之后,首先进行精确查找,如果直接找到,则直接返回找到的内存块。
-
如果精准查找失败,则逐渐增大start_list,从更大的内存块链表中查找。
GC_allochblk_nth方法是核心的内存块查找方法,代码较复杂,主要逻辑如下:
STATIC struct hblk *GC_allochblk_nth(size_t sz, int kind, unsigned flags, int n, int may_split) { struct hblk *hbp; hdr * hhdr; struct hblk *thishbp; hdr * thishdr;/* Header corr. to thishbp */ //计算需要分配的内存块大小 signed_word size_needed = HBLKSIZE * OBJ_SZ_TO_BLOCKS_CHECKED(sz); //从链表中查找合适的内存块 for (hbp = GC_hblkfreelist[n];; hbp = hhdr -> hb_next) { signed_word size_avail; if (NULL == hbp) return NULL; //获取内存块的header信息 GET_HDR(hbp, hhdr); //内存块大小 size_avail = (signed_word)hhdr->hb_sz; if (size_avail < size_needed) continue; //可用内存大于需要的分配的大小 if (size_avail != size_needed) { //要求精准不分割,退出循环,返回空 if (!may_split) continue; ... if( size_avail >= size_needed ) { ... //分割内存块,修改链表 hbp = GC_get_first_part(hbp, hhdr, size_needed, n); break; } } } if (0 == hbp) return 0; ... //修改header信息 setup_header(hhdr, hbp, sz, kind, flags) ... return hbp; }
主要逻辑分为以下几步:
-
入参sz是上层需要分配的大小,例如16字节,king是内存类型。例如NORMAL,n是链表GC_hblkfreelist的index,即之前计算出来的内存块index,may_split表示是否允许拆分,如果不允许拆分,则不允许查找更大的内存块。
-
计算需要分配的内存,这里重新计算一次,例如小内存块sz=16字节,则size_needed=4096。
-
遍历查找GC_hblkfreelist[n]的链表中的内存块,由GC_allochblk方法可知,第一次查找是精确查找,例如sz=16,则start_list计算是1,即size_needed=4096,则遍历GC_hblkfreelist[1]的链表,链表中的内存块的大小是4096。如果发现有,停止遍历。
-
如果GC_hblkfreelist[1]的链表中不存在空闲的4KB内存块,逐步从更大的链表中查找,如果发现有,则停止遍历,并且将这个大内存块拆分成2份,取走前半部分想要的内存块。例如找了一个8KB的内存块,拆分这个内存块,过程如图: 找到8KB的内存块,首先将其从8KB链表中删除。因为只需要4KB的内存块,所以将8KB内存块拆分成2半,前半部分用于返回,并且通过修改flag标记~FREE_BLK,标记为非空闲状态。后半部分是新生成的,同时会生成相应的header信息,并加入4KB内存块链表,即GC_hblkfreelist[1],代码逻辑如下:
STATIC struct hblk *GC_get_first_part(struct hblk *h, hdr *hhdr, size_t bytes, int index) { word total_size = hhdr -> hb_sz; struct hblk * rest; hdr * rest_hdr; //从空闲链表删除 GC_remove_from_fl_at(hhdr, index); if (total_size == bytes) return h; //后半部分 rest = (struct hblk *)((word)h + bytes); //生成header信息 rest_hdr = GC_install_header(rest); //内存块大小 rest_hdr -> hb_sz = total_size - bytes; rest_hdr -> hb_flags = 0; ... //加入相应的空闲链表 GC_add_to_fl(rest, rest_hdr); }
-
修改这个内存块对应的header信息,通过修改header重置信息,用于后续的判断。例如修改hb_sz字段,因为实际使用为16字节,则hb_sz改为16,hb_block字段设置为对应的内存块地址,同时清空hb_marks和hb_n_marks字段,用于后续的GC标记。
static GC_bool setup_header(hdr * hhdr, struct hblk *block, size_t byte_sz, int kind, unsigned flags) { hhdr -> hb_sz = byte_sz; hhdr -> hb_obj_kind = (unsignedchar)kind; hhdr -> hb_flags = (unsignedchar)flags; hhdr -> hb_block = block; descr = GC_obj_kinds[kind].ok_descriptor; if (GC_obj_kinds[kind].ok_relocate_descr) descr += byte_sz; hhdr -> hb_descr = descr; ... GC_clear_hdr_marks(hhdr); hhdr -> hb_last_reclaimed = (unsignedshort)GC_gc_no; return(TRUE); }
-
内存块分配
如果GC_hblkfreelist空闲链表中找不到合适的内存块,则考虑从系统开辟一段新的内存,并添加到GC_hblkfreelist链表中。在GC_expand_hp_inner方法中实现:
GC_INNER GC_bool GC_expand_hp_inner(word n)
{
...
//调用系统方式开辟内存
space = GET_MEM(bytes);
//记录内存地址和大小
GC_add_to_our_memory((ptr_t)space, bytes);
...
//添加到GC_hblkfreelist链表中
GC_add_to_heap(space, bytes);
...
}
-
GET_MEM是一个宏,对于不同的操作系统,分配内存的接口不一样,该方法会根据操作系统平台调用相应的系统接口,例如malloc分配内存。
-
GC_add_to_our_memory方法将这次分配的内存放到一个全局数组中维护,通过这个数组,可以查询到分配的内存块信息。
# define GC_our_memory GC_arrays._our_memory
-
GC_add_to_heap方法将创建出来的内存块加入相应的GC_hblkfreelist链表中。同时加入一个全局的存放堆内存信息的数组中。
GC_INNER void GC_add_to_heap(struct hblk *p, size_t bytes) { ... //生成header信息 phdr = GC_install_header(p); //堆内存信息记录 GC_heap_sects[GC_n_heap_sects].hs_start = (ptr_t)p; GC_heap_sects[GC_n_heap_sects].hs_bytes = bytes; GC_n_heap_sects++; //更新hb_sz和hb_flags phdr -> hb_sz = bytes; phdr -> hb_flags = 0; //加入链表中 GC_freehblk(p); ... //更新堆内存地址的范围,用于后续GC GC_collect_at_heapsize += bytes; GC_least_plausible_heap_addr = (void *)((ptr_t)p - sizeof(word)); GC_greatest_plausible_heap_addr = (void *)endp; }
其中GC_freehblk方法有一些特殊的逻辑,发现内存连续的前后内存块是否存在且空闲,则合并前后的内存块,生成一个更大的内存块。
GC_INNER void GC_freehblk(struct hblk *hbp) { struct hblk *next, *prev; hdr *hhdr, *prevhdr, *nexthdr; word size; GET_HDR(hbp, hhdr); size = HBLKSIZE * OBJ_SZ_TO_BLOCKS(hhdr->hb_sz); hhdr->hb_sz = size; hhdr -> hb_flags |= FREE_BLK; next = (struct hblk *)((ptr_t)hbp + size); GET_HDR(next, nexthdr); prev = GC_free_block_ending_at(hbp); //合并next if (0 != nexthdr && HBLK_IS_FREE(nexthdr)) { GC_remove_from_fl(nexthdr); hhdr -> hb_sz += nexthdr -> hb_sz; GC_remove_header(next); } //合并prev if (0 != prev) { prevhdr = HDR(prev); GC_remove_from_fl(prevhdr); prevhdr -> hb_sz += hhdr -> hb_sz; GC_remove_header(hbp); hbp = prev; hhdr = prevhdr; } ... //加入空闲链表 GC_add_to_fl(hbp, hhdr); }
其中GC_add_to_fl方法负责将新分配的内存块添加到对应的空闲链表GC_hblkfreelist,代码如下:
STATIC void GC_add_to_fl(struct hblk *h, hdr *hhdr) { int index = GC_hblk_fl_from_blocks(divHBLKSZ(hhdr -> hb_sz)); ... GC_hblkfreelist[index] = h; ... }
GC_hblk_fl_from_blocks方法如上文所述,决定创建的内存块放到哪一个GC_hblkfreelist链表中管理。
hblk内存块->ok_freeList
得到了合适的内存块并返回给上层,需要利用这个内存块构建上层的空闲链表ok_freelist。在GC_new_hblk中调用GC_build_fl方法构建链表。
//构建ok_freelist[gran]
GC_obj_kinds[kind].ok_freelist[gran] = GC_build_fl(h, GRANULES_TO_WORDS(gran), clear,(ptr_t)GC_obj_kinds[kind].ok_freelist[gran]);
GC_INNER ptr_t GC_build_fl(struct hblk *h, size_t sz, GC_bool clear,
ptr_t list) {
word *p, *prev;
word *last_object;/* points to last object in new hblk*/
...
//构建链表
p = (word *)(h -> hb_body) + sz;/* second object in *h*/
prev = (word *)(h -> hb_body);/* One object behind p*/
last_object = (word *)((char *)h + HBLKSIZE);
last_object -= sz;
while ((word)p <= (word)last_object) {
/* current object's link points to last object */
obj_link(p) = (ptr_t)prev;
prev = p;
p += sz;
}
p -= sz;
//拼接之前的链表
*(ptr_t *)h = list;
//返回入口地址
return ((ptr_t)p);
}
划分构建ok_freelist的流程如图所示:
以4096字节的内存块划分为16字节单元的freeList为例,步骤如下:
-
4096字节按照16字节分给,划分为256个小内存块,编号是0~255,将最后一个内存块(255)作为新链表的首节点。
-
内存地址向前遍历,建立链表,即255的下一个节点是254,尾节点是0。
-
将尾节点的下一个节点指向原链表的首地址。
-
将新链表的首节点地址作为ok_freelist[N],N是上文提到的GRANULE,例如16字节对应1。
重建好的freeList,并将首节点提供给上层使用。
大对象内存分配
不同于分配小内存对象(2048字节以内),分配大内存对象是指分配的内存大于2048字节,回到GC_generic_malloc方法:
GC_API GC_ATTR_MALLOC void * GC_CALLGC_generic_malloc(size_t lb, int k)
{
...
if (SMALL_OBJ(lb)) {
}
else {
lg = ROUNDED_UP_GRANULES(lb);
lb_rounded = GRANULES_TO_BYTES(lg);
//计算hblk内存块的个数
n_blocks = OBJ_SZ_TO_BLOCKS(lb_rounded);
result = (ptr_t)GC_alloc_large(lb_rounded, k, 0);
...
}
retrun op;
}
OBJ_SZ_TO_BLOCKS用于计算需要的hblk内存块的个数,对于大内存,需要的个数大于等于1。例如需要分配9000字节的内存,则需要3个hblk内存块,然后调用GC_alloc_large分配内存。
GC_alloc_large方法和注释如下:
GC_INNER ptr_t GC_alloc_large(size_t lb, int k, unsigned flags)
{
struct hblk * h;
word n_blocks;
ptr_t result;
...
n_blocks = OBJ_SZ_TO_BLOCKS_CHECKED(lb);
...
//分配内存
h = GC_allochblk(lb, k, flags);
...
//分配失败,系统分配内存块后继续尝试分配
while (0 == h && GC_collect_or_expand(n_blocks, flags != 0, retry)) {
h = GC_allochblk(lb, k, flags);
retry = TRUE;
}
//记录大内存创建大小
size_t total_bytes = n_blocks * HBLKSIZE;
...
GC_large_allocd_bytes += total_bytes;
...
result = h -> hb_body;
//返回内存地址
return result;
}
lb是需要分配的内存大小,然后回到了GC_allochblk核心方法的调用。
查找空闲内存快
重新看一下GC_allochblk方法的实现。
struct hblk * GC_allochblk(size_t sz, int kind, unsigned flags)
{
word blocks;
int start_list;
struct hblk *result;
int may_split;
...
//计算需要的HBLK内存块个数
blocks = OBJ_SZ_TO_BLOCKS_CHECKED(sz);
...
//处理个数
start_list = GC_hblk_fl_from_blocks(blocks);
//精确分配
result = GC_allochblk_nth(sz, kind, flags, start_list, FALSE);
if (0 != result) return result;
...
//从更大的内存中查找
for (; start_list <= split_limit; ++start_list) {
result = GC_allochblk_nth(sz, kind, flags, start_list, may_split);
if (0 != result) break;
}
return result;
}
和小内存对象查找内存块的逻辑一样,首先通过GC_hblk_fl_from_blocks计算得到start_list,即从链表GC_hblkfreelist[start_list]开始查找,如果找不到,则不断增加start_list,从更大的链表中查找。
GC_allochblk_nth的逻辑和小内存查找一样,不同的是,大内存块分配好后,不会进一步用于构建ok_freeList链表,而是直接返回大内存块的地址。
内存分配总体流程
综上,内存分配的流程图如下:
Header信息存储逻辑
如上文所述,对于每一个从GC_hblkfreelist中的内存块,都有一个与之对应的Header数据,用于描述这个内存块的信息,这个header信息也是GC算法逻辑实现的关键。分配了的内存块和Header之后,通过SET_HDR方法存储header信息,相应的GET_HDR方法可以找到header信息。
二维结构
Header信息存放在一个全局的二级数组中,全局信息的数据结构如下:
#define GC_top_index GC_arrays._top_index
#ifndef HASH_TL
# define LOG_TOP_SZ (WORDSZ - LOG_BOTTOM_SZ - LOG_HBLKSIZE)
#else
# define LOG_TOP_SZ 11
#endif
struct GC_arrays {
...
bottom_index *_top_index[TOP_SZ];
};
typedef struct bi {
hdr * index[BOTTOM_SZ];
structbi * asc_link; /* All indices are linked in*/
/* ascending order... */
structbi * desc_link;/* ... and in descending order. */
wordkey; /* high order address bits. */
# ifdef HASH_TL
structbi * hash_link;/* Hash chain link. */
# endif
} bottom_index;
全局对象GC_arrays维护了数组_top_index,包含一组bottom_index指针,每个bottom_index维护了一个数组index,里面存放各个Header数据。
计算规则
对于一个内存块地址p,通过hash算法,计算出存储header的位置。流程如下:
-
已知一个64位的内存块地址p,将其分为3段,利用每一个段的信息确定其位置。如图所示: 内存地址从高到低分别是,高42位,22~63位,用于计算top_index数组中的下标index。
确定了bottom_index之后,内存地址12~22位,用于计算bottom_index中index数组的下标。
内存地址0~12,用于计算具体的对象内存地址在这个内存块中的索引。
此外,算法设置了一个宏HASH_TL,开启和关闭这个宏,算法逻辑略有差异,以开启HASH_TL宏为例,逻辑如下:
#ifndef HASH_TL # define LOG_TOP_SZ (WORDSZ - LOG_BOTTOM_SZ - LOG_HBLKSIZE) #else # define LOG_TOP_SZ 11 #define TOP_SZ (1 << LOG_TOP_SZ) #define HDR_FROM_BI(bi, p) ((bi)->index[((word)(p) >> LOG_HBLKSIZE) & (BOTTOM_SZ - 1)]) # define TL_HASH(hi) ((hi) & (TOP_SZ - 1)) # define GET_BI(p, bottom_indx) \ do { \ REGISTER word hi = (word)(p) >> (LOG_BOTTOM_SZ + LOG_HBLKSIZE); \ REGISTER bottom_index * _bi = GC_top_index[TL_HASH(hi)]; \ while (_bi -> key != hi && _bi != GC_all_nils) \ _bi = _bi -> hash_link; \ (bottom_indx) = _bi; \ } while (0) # define GET_HDR_ADDR(p, ha) \ do { \ REGISTER bottom_index * bi; \ GET_BI(p, bi); \ (ha) = &HDR_FROM_BI(bi, p); \ } while (0) # define GET_HDR(p, hhdr) \ do { \ REGISTER hdr ** _ha; \ GET_HDR_ADDR(p, _ha); \ (hhdr) = *_ha; \ } while (0) # define SET_HDR(p, hhdr) \ do { \ REGISTER hdr ** _ha; \ GET_HDR_ADDR(p, _ha); \ *_ha = (hhdr); \ } while (0)
-
其中GET_BI (p, bi) 用于计算topIndex数组中的下标,即计算得到对应的bottom_indx对象,结合上图可以看到,通过 (p) >> (LOG_BOTTOM_SZ + LOG_HBLKSIZE) 向右偏移22位,得到高42位,然后通过TL_HASH(hi),取其中的11位,作为hash值,定位到GC_top_index[hash],由于不同内存地址的中间11位可能相同,因此维护一个hash链表,从GC_top_index[hash]作为链表的入口,然后遍历匹配bi->key,key存放的是TL_HASH之前的42位信息。匹配到之后,定位具体的bottom_indx信息。
-
调用HDR_FROM_BI(bi, p) 方法,取内存地址12~22位,作为hash值,定位bi->index[hash]作为header信息的存放地址。
-
同时还提供了2个方法,
# define HBLKSIZE 4096 # define HBLKPTR(objptr) ((struct hblk *)(((word)(objptr)) & ~(word)(HBLKSIZE-1))) # define HBLKDISPL(objptr) (((size_t) (objptr)) & (HBLKSIZE-1))
-
HBLKPTR用于返回当前对象内存地址对应的hblk内存块的首地址,因为根据上文,分配内存的基本单位都是hblk,大小是4096字节,即分配的内存一定是4096的倍数,因此和“~(HBLKSIZE-1)”进行“按位与操作”可以得到内存对象属于的那个hblk地址,对象p的地址位于hblk中。
-
HBLKDISPL用于返回当前对象内存地址在hblk内存块中的偏移量。因为有效的对象内存地址一定在一个hblk中,一个hblk的内存大小是0~4096,12位,因此和“HBLKSIZE-1”进行“按位与操作”可以得到偏移量。
-
-
以16字节的小内存对象为例,删除一个4KB的hblk可以分配256个这样的对象,则对象N的内存地址是HBLK的首地址+16*N。
辅助链表
另外还维护了一个一维双向链表来辅助管理所有的bottom_index数据,在分配内存块,生成header信息时,将新的bottom_index节点插入链表中合适的位置。按照链表是有序链表,bottom_index节点按照bottom_index->key字段值递增,key字段存的是前42位地址,因此内存地址越大,链表中的位置越靠后。如图所示:
插入新的bottom_index节点到链表的过程如下:
static GC_bool get_index(word addr)
{
word hi = (word)(addr) >> (LOG_BOTTOM_SZ + LOG_HBLKSIZE);
bottom_index * r;
bottom_index * p;
bottom_index ** prev;
bottom_index *pi; /* old_p */
word i;
# ifdef HASH_TL
i = TL_HASH(hi);
pi = p = GC_top_index[i];
while(p != GC_all_nils) {
if (p -> key == hi) return(TRUE);
p = p -> hash_link;
}
# else
if (GC_top_index[hi] != GC_all_nils)
return TRUE;
i = hi;
# endif
//创建bottom_index节点
r = (bottom_index *)GC_scratch_alloc(sizeof(bottom_index));
if (EXPECT(NULL == r, FALSE))
return FALSE;
//初始化清空数据
BZERO(r, sizeof(bottom_index));
r -> key = hi;
# ifdef HASH_TL
//r放置在GC_top_index[i]中第一个位置
r -> hash_link = pi;
# endif
prev = &GC_all_bottom_indices;
pi = 0;
//查找第一个大于hi的节点p
while ((p = *prev) != 0 && p -> key < hi) {
pi = p;
prev = &(p -> asc_link);
}
//将r插入链表中
r -> desc_link = pi;
if (0 == p) {
GC_all_bottom_indices_end = r;
} else {
p -> desc_link = r;
}
r -> asc_link = p;
*prev = r;
//r放置在GC_top_index[i]中第一个位置
GC_top_index[i] = r;
return(TRUE);
}
缓存结构
维护了一个缓存结构,存储一个内存块及其对应的header信息,数据结构如下:
typedef struct hce {
word block_addr; //内存块地址
hdr * hce_hdr; //header地址
} hdr_cache_entry;
查找header信息时,也可以通过先查找缓存的方式增加效率。通过宏HC_GET_HDR封装。
#define HC_GET_HDR(p, hhdr, source) \
{ /* cannot use do-while(0) here */ \
hdr_cache_entry * hce = HCE(p); \
if (EXPECT(HCE_VALID_FOR(hce, p), TRUE)) { \
HC_HIT(); \
hhdr = hce -> hce_hdr; \
} else { \
hhdr = HEADER_CACHE_MISS(p, hce, source); \
if (NULL == hhdr) break; /* go to the enclosing loop end */ \
} \
}
如果找到,直接返回,找不到再通过GC_header_cache_miss方法查找。GC_header_cache_miss方法通过HDR(p)方法查找header信息。
总结
这篇文章大致分析了Unity中BoehmGC算法关于对象内存分配的实现思路,下一篇文章将学习和分析GC的实现。其中的许多细节还有待进一步学习和挖掘。另外,文章中理解有误之处,希望大家多多指正。