上一篇文章中,我们实现了自定义分配内存,有了这个基础,我们可以开发垃圾回收算法了。GC算法有很多种,如引用计数法、标记-清除算法、复制算法、分代回收算法等,也有综合运用几种算法的。PHP用到了引用计数算法,Java用到了复制算法和分代回收算法。由于引用计数算法需要频繁更新引用计数,目前暂不研究;标记-清除算法则因为清除后造成大量内存碎片不好管理,目前只研究标记(标记出活动对象);复制算法是本篇研究的重点。
1.标记-清除算法、复制算法简介
1.1 根对象
首先需要理解一个基本概念:根对象。根对象是程序中可以直接访问到的对象,比如:
有两个对象
a
,a
有成员b
,访问对象a
直接用a
就行,而访问对象b需要通过a->b
,那么对象a
就是根对象,对象b
由于只能通过a->b
来访问,所以不是根对象。
根对象可以是全局变量、函数调用栈上的变量等。
1.2 标记-清除算法
标记-清除算法的大致思想如下:
标记阶段:遍历根对象及其引用的对象。假设每个对象都有个标记位
flag
,对根对象集合中的每个根对象,从根对象出发,对可以访问到的每个对象的标记位flag
设为1(活动对象)。清除阶段:遍历堆,将非活动对象所占空间设为可用。遍历堆,将标记位
flag
等于0的对象(即垃圾)所占据的空间设为可用。
标记-清除算法的清除阶段 后,产生很多内存碎片,管理比较麻烦。
1.3 复制算法
复制算法的大致思想如下:
把给对象分配内存的堆(heap)分成大小相等的两部分,或者申请2块大小相同的堆,其中一个堆称为
From空间
,另一个称为To空间
。首次给对象分配内存时,活动堆为
From空间
,从From空间
分配;触发GC时,采用标记-清除算法中的标记算法遍历活动对象,把活动对象复制到To空间
,然后就把To空间
当做当前活动堆。To空间
满触发GC时,把活动对象复制到From空间
,如此交替进行。
复制算法的缺点是内存空间利用效率低,只有50%。
2.标记对象
先构建一个测试场景,代码如下:
Object *root[2]; // simulate root objects collection
void test_alloc_memory() {
Object *objects[6];
int obj_len[6] = {3,2,4,2,3,2};
int i;
for(i=0; i<6; i++) {
objects[i] = new_object(obj_len[i]);
OBJ_SET_INT(objects[i], 0, i);
}
OBJ_SET_OBJ(objects[1], 1, objects[4]); // objects[1]->objects[4]
OBJ_SET_OBJ(objects[0], 1, objects[1]); // objects[0]->objects[1]
OBJ_SET_OBJ(objects[0], 2, objects[5]); // objects[0]->objects[5]
root[0] = objects[0];
root[1] = objects[2]; // objects[3] is garbage
}
为了简化问题,当前用一个对象数组root
来表示根对象集合。
objects[0]
会引用到objects[1]
、objects[4]
和、objects[5]
。我们只把objects[0]
和objects[2]
添加到根对象集合中,因为objects[3]
无法从根对象中访问到,因此,objects[3]
是“垃圾”(不可达的对象就是垃圾)。
以上代码形成的引用关系示意图如下:
上图中,object[0]
中的方括号没有数组的含义,仅仅是一个符号而已,它指的就是alloc_memory
函数中的objects[0]
。objref[0]
是指向object[0]
的引用,objref[2]
是指向`object[2]
的引用。
标记算法可实现为如下:
void mark_object(Object *obj) {
Object *sub_obj = NULL;
int index;
obj->flag = 1; // active
for(index=1; index<obj->length; index++) {
sub_obj = OBJ_GET_OBJ(obj, index);
if (NULL != sub_obj) {
mark_object(sub_obj);
}
}
}
void mark_objects(Object *root[], int len) {
int i;
for(i=0; i<len; i++) {
mark_object(root[i]);
}
}
本篇文章中的代码是基于上一篇文章的,所以,代码方面有疑问的话需要先阅读上篇文章。
关键点如下:
- 对象的结构模型仍然是按上篇文章中简化后的模型,即第一个字段的数据类型为
int
,后续的字段类型为Object
。 - 函数
mark_object
用于标记单个根对象,以及该根对象所引用到的对象,其中用到了递归。将对象的flag
字段设为1,表示该对象为活动对象。未标记的对象的flag
为0。 - 函数
mark_objects
中,遍历根对象集合,堆每个跟对象依次调用函数mark_object
,即完成所有对象的标记。
组织测试:
int main() {
init_heap();
printf("after alloc...");
test_alloc_memory();
dump_heaps();
printf("after mark...");
mark_objects(root, 2);
dump_heaps();
return 0;
}
运行结果如下:
分配内存后堆的打印结果:
标记后堆的打印结果:
对比上面的 “引用关系示意图”,可知“垃圾对象” object[3]
识别出来了。
3.复制算法
复制算法可以采用深度优先搜索(dfs),也可以用广度优先搜索(bfs)。这两种方法复制后对象的物理位置顺序不一样。以object[0]
的复制为例:
深度优先搜索的复制算法的思路如下:
复制一个根对象:
- 从
To空间
中找到空闲空间的起始地址,- 复制根对象到该起始地址
- 设置原对象的
flag
为2,表示已复制,避免重复复制。设置复制后对象的flag
为0,表示初始状态- 遍历该对象所直接引用的对象,如果没有复制的话就递归调用该函数;关键:复制后要更新新对象的引用。
复制所有根对象:
遍历根对象集合,对每个根对象调用“复制根对象”的函数,然后更新根对象集合的引用。
写成代码就是:
// copy a root object and the objects referenced by it
Object* copy_object_dfs(Object *obj) {
Object *sub_obj;
Object *new_obj;
int index;
uint size = OBJ_SIZE(obj);
char* new_addr = alloc_memory(free_heap, size);
memcpy(new_addr, obj, size);
new_obj = (Object*)new_addr;
OBJ_SET_FIELDS(new_obj, new_addr);
obj->flag = 2; // copied
new_obj->flag = 0;
// copy each direct referenced object
for(index=1; index<obj->length; index++) {
sub_obj = OBJ_GET_OBJ(obj, index);
if (NULL != sub_obj && sub_obj->flag != 2) {
// update reference
OBJ_SET_OBJ(new_obj, index, copy_object_dfs(sub_obj));
OBJ_SET_OBJ(obj, index, sub_obj);
}
}
return new_obj;
}
// copy all root objects
void copy_objects_dfs(Object *root[], int len) {
int i;
for(i=0; i<len; i++) {
root[i] = copy_object_dfs(root[i]);
}
}
其中,宏OBJ_SET_FIELDS
定如下:
#define OBJ_SET_FIELDS(obj,new_addr) (obj)->fields = new_addr + sizeof(Object)
对象复制过程示意图如下:
复制完成后内存中个对象的引用关系示意图如下:
箭头颜色说明:
- 蓝色,表示原有的引用关系
- 红色,表示未更新的引用关系
- 绿色,表示更新后的引用关系
4.测试
如何验证复制是OK的呢?在复制的实现代码中,我们用到了memcpy
函数,该函数会忠实地拷贝内存。检验复制成功考虑以下三个指标:
- 基本数据类型的值保持不变
- 指针的指向关系保持不变
- 最后,该复制的对象都要复制,不能有遗漏,或者复制了“垃圾对象”。
组织测试:
int main() {
init_heap();
printf("after alloc...");
test_alloc_memory();
dump_heaps();
printf("after copy...");
copy_objects_dfs(root, 2);
dump_heaps();
dump_active_objects(root, 2);
return 0;
}
运行结果:
复制前活动堆(From空间)的情况:
复制后空闲堆(To空间)的情况:
因为object[3]
是“垃圾对象”,所以没有复制。
仔细比较复制前后堆的数据,以及对象之间的引用关系,可知复制是OK的。
把活动对象打印出来看下:
也是OK的。
5.总结
在基于上篇自定义内存分配的基础上,本文实现了:
- 正确标记对象(活动对象与“垃圾对象”)
- GC中的复制算法(用深度优先搜索)。当然可以用广度优先搜索,限于篇幅就不写出来了。