要实现自己的垃圾回收算法,首先要实现一套自己的内存分配方法,把内存的管理权掌握在自己手里,而不是每次都调用系统函数,向操作系统要一小块内存,否则垃圾回收就无从谈起。思路主要是:
一开始申请一块大的内存,后面每次程序需要内存就从这个内存块中分配,不够了再想办法处理(垃圾回收、压缩、分配更大的内存等)。
上面申请的一块大的内存一般叫做堆(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种数据类型,int
和Object
。
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 创建对象
解决了给对象分配内存的问题,接着就是要创建对象。创建对象的基本步骤如下:
- 计算该对象所需空间大小
- 分配内存空间
- 初始化对象的头部信息以及字段内容
/**
* 从当前可用堆中创建一个对象
* @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),后续的动态内存分配均在该堆中进行,运行正常。无需反复调用系统函数进行分配内存。