现在工作主要是游戏方面,游戏开发就必然绕不开游戏引擎,自己使用的是Unity的引擎,Unity引擎使用可以使用的语言也有多种,本人使用C# 而且跑在Mono-Runtime 上。Mono和Unity的关系不在赘述,本文默认你有C#的编写基础,跨平台语言基础。
什么是GC
GC 全称 Garbage Collection。也叫垃圾回收。在我们写C#或是Java代码时候我们并不关心一个对象在内存中的创建和释放,我们只是通过一个关键词new 来实例化出一个对象来,至于在内存中是如何被分配是不关心的,我们也更加不关心最后是如何释放这段内存的了。而在写C/C++代码时候我们必须需要手动的管理内存的申请和释放,因为C/C++默认就是需要程序员来自己管理,因为他相信的是程序员。那么在C#、JAVA中为何就不必关心了呢?就是因为存在GC技术,让你不必再过多的关注这块内容,那么为什么垃圾GC会帮我们自动回收呢?背后的原理是什么?
GC的原理
如果把程序员手工的对一系列对象进行创建释放内存当做一次垃圾回收,那么Mono平台提供的垃圾回收器,无非是一种自动操作,他代替人工去书写代码判断创建释放内存。无非就是自动在如何判断上。如何判断一个对象是否需要被回收就是GC的策略算法了一般有以下几种:
1.引用计数
- 每一个对象都有一个引用计数器,当一个对象被引用时候,计数器就会+1,当不被引用时候,计数器就-1,当所有的引用都没有时候,那么计数器就会归0,那么这个对象就会被认为不再使用了,会回收此对象。缺点就是如果有两个对象,相互引用了对方,那么他们的计数器就永远不会归0了,那么这两个对象就永远不会被删除了。在Android的源码中的强指针和弱指针的实现就是使用了计数引用,所以为了解决相互引用的问题,其源码中如果两个对象相互引用就,必须有一个对象是弱指针。
2.根搜索
- 根搜索就是从一个根开始遍历所有的叶子,然后把所有不需要的叶子清除。此中又可以分出1.Mark-Sweep 2.Copying 3.Mark-Compact 。这几种区分无非是在对内存进行回收后如何处理内存分布的一些策略。
Mono中的GC
Mono 在2.10版本前都是使用了BOEHM的垃圾回收器。而在2.10后的版本中使用了一个叫SGen的垃圾回收器。而我们的Unity中Mono的版本一直停留在2.10版本之前,所以一直集成的是BOEHM的GC。BOEHM GC是一个开源的项目,在使用中只要替换分配内存函数就可以实现C/C++项目的内存自动管理。
BOEHM GC
BOEHM GC是一个保守式GC,与之对应的还有准确式GC。所谓保守式GC就是说其无法分辨一个Root 是一个指针还是非指针,所以会对所有当做一个指针做尝试,所以无法使用Copying算法。在BOEHM GC为了增加分辨能力其会根据已经分配过的内存区间做比较,增加指针黑名单,来增加分辨率。而准确式GC就是GC可以直接分辨出这是个指针还是非指针。而且可以使用Copying算法优化内存的使用。 BOEHM GC使用的是Mark-Sweep,也就是先通过一个Root指针来遍历所有的被引用的对象,并标记。直到遍历完所有的指针。再次遍历整个,将未标记的内存释放。
BOEHM GC内存分配
上面已经说了,在项目中使用BOEHM GC只要替换原来的内存分配函数就可以,那么我们就跟着GC_MALLOC进入代码。
GC_PTR GC_malloc(size_t lb)
size_t lb;
{
register ptr_t op;
register ptr_t *opp;
register word lw;
//判断是否为小对象,小对象的定义根据定义可以知道是(1<<12)/2 也就是2048字节。
if( EXPECT(SMALL_OBJ(lb), 1) ) {
//小对象分配
lw = GC_size_map[lb];
opp = &(GC_objfreelist[lw]);
FASTLOCK();
if( EXPECT(!FASTLOCK_SUCCEEDED() || (op = *opp) == 0, 0) ) {
FASTUNLOCK();
return(GENERAL_MALLOC((word)lb, NORMAL));
}
/* See above comment on signals. */
GC_ASSERT(0 == obj_link(op)
|| (word)obj_link(op)
<= (word)GC_greatest_plausible_heap_addr
&& (word)obj_link(op)
>= (word)GC_least_plausible_heap_addr);
*opp = obj_link(op);
obj_link(op) = 0;
GC_words_allocd += lw;
FASTUNLOCK();
return((GC_PTR) op);
} else {
//大对象分配
return(GENERAL_MALLOC((word)lb, NORMAL));
}
}
从这段代码可以看出当调用内存分配的时候,BOEHM首先会根据需要分配的内存大小来做选择,小于2048字节的会有小对象分配逻辑,大于的会有大对象分配逻辑。小对象会先根据缓存情况,然后如果已经有空闲的就直接返回,并且将分配完的东西移除freelist,如果没有再进行分配。GC_MALLOC —> GC_generic_malloc_inner
ptr_t GC_generic_malloc_inner(lb, k)
register word lb;
register int k;
{
register word lw;
register ptr_t op;
register ptr_t *opp;
if( SMALL_OBJ(lb) ) {
//对应的分配类型kind获取
register struct obj_kind * kind = GC_obj_kinds + k;
//获取对应的分配倍数
lw = GC_size_map[lb];
//获取对应的ok_freelist
opp = &(kind -> ok_freelist[lw]);
if((op = *opp) == 0) {
//如果倍数计算是0,进行初始化和size_map扩展,并再次调用自身
if (GC_size_map[lb] == 0) {
if (!GC_is_initialized) GC_init_inner();
if (GC_size_map[lb] == 0) GC_extend_size_map(lb);
return(GC_generic_malloc_inner(lb, k));
}
if (kind -> ok_reclaim_list == 0) {
if (!GC_alloc_reclaim_list(kind)) goto out;
}
//进入下一步
op = GC_allocobj(lw, k);
if (op == 0) goto out;
}
/* Here everything is in a consistent state. */
/* We assume the following assignment is */
/* atomic. If we get aborted */
/* after the assignment, we lose an object, */
/* but that's benign. */
/* Volatile declarations may need to be added */
/* to prevent the compiler from breaking things.*/
/* If we only execute the second of the */
/* following assignments, we lose the free */
/* list, but that should still be OK, at least */
/* for garbage collected memory. */
*opp = obj_link(op);
obj_link(op) = 0;
} else {
lw = ROUNDED_UP_WORDS(lb);
op = (ptr_t)GC_alloc_large_and_clear(lw, k, 0);
}
GC_words_allocd += lw;
out:
return op;
}
这段代码进来后还是依旧分为大对象和小对象,我们接下去的分析将都跟着小对象的分配逻辑走。这里的kinds 简单来说就是用来区分不同的分配内存类型,kinds的情况可以有PTRFREE,NORMAL,UNCOLLECTABLE等,这边进来是NORMAL,也就是需要被进行垃圾回收的一些分配情况。每一种的kinds中都会带有一个ok_freelist用来存储分配后的内存列表。每个元素是一个倍数的内存分配链表。GC_generic_malloc_inner —> GC_allocobj
ptr_t GC_allocobj(sz, kind)
word sz;
int kind;
{
//获取ok_freelist的元素对应的倍数分配内存链表头。
ptr_t * flh = &(GC_obj_kinds[kind].ok_freelist[sz]);
GC_bool tried_minor = FALSE;
if (sz == 0) return(0);
while (*flh == 0) {
ENTER_GC();
/* Do our share of marking work */
if(TRUE_INCREMENTAL) GC_collect_a_little_inner(1);
/* Sweep blocks for objects of this size */
GC_continue_reclaim(sz, kind);
EXIT_GC();
//当为空时候创建一个新的倍数分配内存块。
if (*flh == 0) {
GC_new_hblk(sz, kind);
}
if (*flh == 0) {
ENTER_GC();
if (GC_incremental && GC_time_limit == GC_TIME_UNLIMITED
&& ! tried_minor ) {
GC_collect_a_little_inner(1);
tried_minor = TRUE;
} else {
if (!GC_collect_or_expand((word)1,FALSE)) {
EXIT_GC();
return(0);
}
}
EXIT_GC();
}
}
/* Successful allocation; reset failure count. */
GC_fail_count = 0;
return(*flh);
}
此段代码比较简单,看链表如果有对应的free的块就返回,没有就进行分配。GC_allocobj —> GC_new_hblk
void GC_new_hblk(sz, kind)
register word sz;
int kind;
{
register struct hblk *h; /* the new heap block */
register GC_bool clear = GC_obj_kinds[kind].ok_init;
if (GC_debugging_started) clear = TRUE;
/* Allocate a new heap block */
h = GC_allochblk(sz, kind, 0);
if (h == 0) return;
/* Mark all objects if appropriate. */
if (IS_UNCOLLECTABLE(kind)) GC_set_hdr_marks(HDR(h));
/* Build the free list */
GC_obj_kinds[kind].ok_freelist[sz] =
GC_build_fl(h, sz, clear, GC_obj_kinds[kind].ok_freelist[sz]);
}
源码中已经有了注释,主要是两个函数 GC_allochblk 和 GC_build_fl;
struct hblk *
GC_allochblk(sz, kind, flags)
word sz;
int kind;
unsigned flags; /* IGNORE_OFF_PAGE or 0 */
{
word blocks = OBJ_SZ_TO_BLOCKS(sz);
int start_list = GC_hblk_fl_from_blocks(blocks);
int i;
/*遍历查找对应的块有没有空余的*/
for (i = start_list; i <= N_HBLK_FLS; ++i) {
struct hblk * result = GC_allochblk_nth(sz, kind, flags, i);
if (0 != result) {
return result;
}
}
return 0;
}
GC_allochblk_nth这个函数比较长所以暂时省略了。其主要的内容就是在一个GC_hblkfreelist中查找一个空闲的块,GC_hblkfreelist中的元素指向的是一个以PAGE_SIZE为基数乘以index后大小的内存块。比如元素1的是PAGE_SIZE1,元素2的是PAGE_SIZ2的大小。当在list中找到一个空闲块时候,再一个块中拆出一个PAGE_SIZE,然后把剩余的放回到list中。并且被拆出来的PAGE_SIZE会根据大小需求拆成对应的小列表然后返回给上一个函数kind中的ok_freelist的对应元素。这样就完成了一次内存分配。
写在最后
总体来说现在看的也不是非常的透彻,有些代码过于晦涩,能力有限可能有些错误和遗漏。本次文章主要分析的是如何给一个MonoObjcet分配内存,下次文章会分析一下是如何进行回收的。
参考:BOEHM GC