GC算法实践(一) 内存分配篇

要实现自己的垃圾回收算法,首先要实现一套自己的内存分配方法,把内存的管理权掌握在自己手里,而不是每次都调用系统函数,向操作系统要一小块内存,否则垃圾回收就无从谈起。思路主要是:

一开始申请一块大的内存,后面每次程序需要内存就从这个内存块中分配,不够了再想办法处理(垃圾回收、压缩、分配更大的内存等)。

上面申请的一块大的内存一般叫做堆(heap)。名字叫什么不要紧,反正就是内存的一部分,申请下来后都归自己管理了。

很多软件都用类似的方法来管理内存,借鉴市场经济的做法,从多次的少量”购买“变成一次”大量批发“,的确是一种进步。

1. 关键数据结构与基本操作

1.1 堆的结构以及基本操作

目前我们只需要知道堆(heap)的起始地址、堆中空闲内存的起始地址、堆的结束地址即可。

定义一个数据结构来表示堆(各字段含义如其名字所示):

typedef struct _Heap {
    char *start_addr;
    char *free_addr;
    char *end_addr;
}Heap;

示意图如下:

这里写图片描述

创建一个堆可以用如下方法:

Heap* new_heap(uint size) {
    Heap *heap = (Heap*)malloc(sizeof(Heap)+size);
    heap->start_addr = (char*)heap + sizeof(Heap);
    heap->free_addr = heap->start_addr;
    heap->end_addr = heap->start_addr + size;

    return heap;
}

够简单了吧,无须解释。

1.2 对象的结构以及基本操作

堆是用来动态分配内存给对象用的,所以我们还得需要知道对象的结构。参考“自制Java虚拟机”中对象的结构,当前为了测试方便,定义对象的结构如下(包含对象的一些头部信息以及对象的字段指针):

typedef struct _Object {
    char flag;
    ushort length;
    char* fields;
}Object;

示意图如下:

这里写图片描述

关于对象,有如下基本操作:

// 1.获取整个对象的占用空间大小
#define OBJ_SIZE(obj) (sizeof(Object) + (obj->length<<2))

// 2.给对象的一个int类型字段赋值
#define OBJ_SET_INT(obj,offset,value) *(int*)(obj->fields + ((offset)<<2)) = (value)
// 3.给对象的一个Object(引用/指针)类型字段赋值
#define OBJ_SET_OBJ(obj,offset,value) *(Object**)(obj->fields + ((offset)<<2)) = (value)
// 4.取一个int类型的字段
#define OBJ_GET_INT(obj,offset) *(int*)(obj->fields + ((offset)<<2))
// 5. 取一个Object(引用/指针)类型的字段
#define OBJ_GET_OBJ(obj,offset) *(Object**)(obj->fields + ((offset)<<2))

目前为了方便测试,对象的字段只有2种数据类型,intObject

2.分配内存

2.1 内存分配方法

C语言中的malloc函数返回的是所申请内存的起始地址(指针),指针类型的转换需要用户自己完成。我们也可以参考这一方案,从堆中找到空闲内存,如果足够的话就返回空闲内存的起始地址,不够的话,目前为了简化,直接退出应用。

分配内存的函数实现如下:

/**
 * 从指定堆分配size大小的内存
 * @return char* 分配内存的首地址
 * @params Heap* heap 堆
 * @params uint size 内存大小(byte) 
 */
char* alloc_memory(Heap* heap, uint size) {
    char *addr = NULL;
    char *next_free_addr = heap->free_addr + size;
    if (next_free_addr > heap->end_addr) {
        printf("Error! Out Of Memory!\n");
        exit(1);    
    }

    addr = heap->free_addr;
    heap->free_addr = next_free_addr;

    return addr; 
}

给对象分配内存的示意图:
这里写图片描述

2.2 创建对象

解决了给对象分配内存的问题,接着就是要创建对象。创建对象的基本步骤如下:

  1. 计算该对象所需空间大小
  2. 分配内存空间
  3. 初始化对象的头部信息以及字段内容
/**
 * 从当前可用堆中创建一个对象 
 * @return Object* 新创建对象的引用 
 * @params ushort length 对象字段数 
 */
Object* new_object(ushort length) {
    uint field_size = length<<2;
    Object *obj;
    obj = (Object*)alloc_memory(cur_heap, field_size + sizeof(Object));
    obj->flag = 0;
    obj->length = length;
    obj->fields = (char*)obj + sizeof(Object);
    memset(obj->fields, 0, field_size);

    return obj;
}

其中,cur_heap是当前可用的堆(活动堆)。

3.测试辅助方法

给对象分配内存的基本功能已经实现了,接下来需要准备测试。

3.1 打印对象

为了方便调试、测试,需要打印出对象的内容。

为了方便测试,约定对象的第一个字段数据类型为int,后面的字段类型均为Object实际使用中需要用合适的方法来识别对象的字段类型是否为对象(引用/指针)。

void _dump_object(Object *obj, int depth) {
    int i, index;
    Object *sub_obj;
    if (NULL == obj) {
        printf("null\n");
        return;
    }

    for(i=0; i<depth; i++) {
        printf("\t");
    }
    if (depth > 0) {
        printf("=>");
    }

    printf("object[%d] at: %p, flag=%d, length=%d, ", OBJ_GET_INT(obj, 0), obj, obj->flag, obj->length);
    printf("fields[0]=%d\n", OBJ_GET_INT(obj, 0));

    for(index=1; index<obj->length; index++){
        sub_obj = OBJ_GET_OBJ(obj, index);
        if (NULL != sub_obj) {
            _dump_object(sub_obj, depth+1);
        }
    }
}
void dump_object(Object *obj) {
    _dump_object(obj, 0);
}

3.2 打印堆(heap)内容

为了方便调试、测试,需要打印出堆中的对象。

由于是连续分配内存,每个对象也可以计算出总的大小,堆可以作为一个链表来遍历。

void dump_heap(Heap *heap) {
    Object *tmp_obj = NULL;
    char *next_addr = NULL;
    uint sz_object = sizeof(Object);
    printf("----------------------------- heap data ------------------------------\n");
    if (NULL == heap) {
        printf("null\n");
        return;
    }

    printf("heap: [start_addr]=%p, [free_addr]=%p, [end_addr]=%p\n", heap->start_addr, heap->free_addr, heap->end_addr);
    if (heap->free_addr == heap->start_addr) {
        printf("heap has no object!\n");
        printf("----------------------------- End ------------------------------------\n");
        return;
    }

    next_addr = heap->start_addr;
    while(next_addr < heap->free_addr){
        tmp_obj = (Object*)next_addr;
        dump_object(tmp_obj);

        next_addr += (sz_object + (tmp_obj->length << 2));
    }

    printf("----------------------------- End ------------------------------------\n");
}

4. 测试

4.1 内存分配的测试函数

 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]
}

该测试函数中一共创建了6个对象,它们之间的关系为:objects[0] => objects[1] => objects[4], objects[0] => objects[5],objects[2]和objects[3]都是孤立的。

它们在内存中的逻辑关系示意图如下:

这里写图片描述

4.2 其它相关代码

#define HEAP_SIZE 1024 // 定义堆的大小

typedef unsigned short ushort;
typedef unsigned int uint;

Heap *cur_heap, *free_heap; // 作为全局变量; cur_heap表示当前活动堆
// 初始化堆
void init_heap() {
    cur_heap  = new_heap(HEAP_SIZE);
    free_heap = new_heap(HEAP_SIZE);
}

4.3 执行测试

main函数中编写测试代码:

// main函数
int main() {
    init_heap(); // 0.初始化堆

    printf("after alloc...");
    test_alloc_memory(); // 测试动态创建对象
    printf("\ncur_heap:\n");
    dump_heap(); // 打印堆中的对象信息

    return 0;
}

4.4 测试数据与结果

运行结果如下:

这里写图片描述

将打印结果与我们的预期(参考test_alloc_memory中的代码)相比,可知对象的内存分配正常。

5.总结

用C语言实现了自定义内存分配算法(相对于调用malloc而言),只需一次调用系统函数malloc,申请一块较大的内存(Heap),后续的动态内存分配均在该堆中进行,运行正常。无需反复调用系统函数进行分配内存。

猜你喜欢

转载自blog.csdn.net/chunyuan314/article/details/78210017