GC算法实践(二) 对象标记、复制算法

来源:互联网 发布:中秋的数据 编辑:程序博客网 时间:2024/05/16 05:34

上一篇文章中,我们实现了自定义分配内存,有了这个基础,我们可以开发垃圾回收算法了。GC算法有很多种,如引用计数法、标记-清除算法、复制算法、分代回收算法等,也有综合运用几种算法的。PHP用到了引用计数算法,Java用到了复制算法和分代回收算法。由于引用计数算法需要频繁更新引用计数,目前暂不研究;标记-清除算法则因为清除后造成大量内存碎片不好管理,目前只研究标记(标记出活动对象);复制算法是本篇研究的重点。

1.标记-清除算法、复制算法简介

1.1 根对象

首先需要理解一个基本概念:根对象。根对象是程序中可以直接访问到的对象,比如:

有两个对象aa有成员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 collectionvoid 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]的复制为例:

这里写图片描述

深度优先搜索的复制算法的思路如下:

复制一个根对象

  1. To空间中找到空闲空间的起始地址,
  2. 复制根对象到该起始地址
  3. 设置原对象的flag为2,表示已复制,避免重复复制。设置复制后对象的flag为0,表示初始状态
  4. 遍历该对象所直接引用的对象,如果没有复制的话就递归调用该函数;关键:复制后要更新新对象的引用。

复制所有根对象

​ 遍历根对象集合,对每个根对象调用“复制根对象”的函数,然后更新根对象集合的引用。

写成代码就是:

// copy a root object and the objects referenced by itObject* 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 objectsvoid 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中的复制算法(用深度优先搜索)。当然可以用广度优先搜索,限于篇幅就不写出来了。
阅读全文
0 0
原创粉丝点击