大佬的技术博客,拜读了一下,理解的七七八八,做一个记录和研究吧。
文章地址:Baby’s
First Garbage Collector – journal.stuffwithstuff.com
简易的垃圾回收机制
在计算机运行的过程中,会不断的分配和释放内存。尤其是在一些低级的语言中,内存的分配和释放都需要程序员手动分配。如果不对内存加以分配和管理,就会导致系统资源耗尽,从而发生内存泄露。或者如果错误的释放了正在使用的内存空间,那么又会导致系统崩溃。所以一个合理的垃圾回收机制,可以自动识别并回收不再使用的内存,减轻程序员的负担。
垃圾回收的几种实现
- 引用计数:为每个对象维护一个引用计数器,初始化为1,每当有一个新的引用指向它,计数器就+1。每当一个引用失效,则将计数器-1。当计数器为0时,说明对象不再被引用,就被垃圾回收器回收了。
- 标记-清除算法:从一个”根对象”开始,通过引用关系遍历所有可以到达的对象,并将其标记为“存活”。然后回收器再扫描一遍内存空间,找出没有被标记的”垃圾”,并将其回收
- 复制算法:将内存分为两块,分别是“从空间”和“到空间”,再内存分配时,只使用从空间,当从空间被填满时,回收器开始工作,其从根对象开始遍历所有的可达对象,并将其复制到到空间。然后将从空间和到空间的身份进行互换,继续使用从空间进行内存的分配
- 标记-整理算法:首先从根对象开始遍历所有可达对象,然后移动存活对象到内存空间的另一端,并更新他们的引用。最后回收器,回收所有没有使用的内存碎片。相当于复制算法和标记-清除算法的结合版
这里我们将要使用的垃圾回收机制是 标记-清除算法
,现在我们需要明确什么是垃圾什么是被使用中的内存空间。
垃圾,指的是之前分配但现在不再使用的内存。使用中的定义则较为复杂,有以下几点:
- 任何处于作用域中的变量所引用的对象都是处于使用中的
- 任何被另一个正在使用中的对象所引用的对象都是处于使用中的(注意理解,这是一条递归规则)
所以我们需要从变量开始遍历对象,以达到所有可以到达的对象,对于不可到达的对象,将其收回。
标记与清除
关键在于对对象的遍历和标记,其原理十分的简单:
- 从根部开始遍历整个对象图。每到达一个对象,就将其上的”标记”设置为true
- 完成后,遍历查找所有的未被设置的对象,并将其删除
对象
垃圾回收器常常被用于各种编程语言中,但是我们这只是演示其功能,我们就只用基本的数据类型,和简单的虚拟机来实现这个功能并创建一些可回收的垃圾。我们用一个枚举变量来标识对象的类型:
1 2 3 4
| typedef enum{ OBJ_INT, OBJ_PAIR } ObjectType;
|
这里定义了两种数据类型,一个是数据类型,一个是成对类型。这个对可以是各种组合,可以是一对数,或者一对对对象,或者一个数和一个对对象。这样的话可以实现对象之间的相互引用,这样的话就可以得到一个连续的引用对象的树,我们我们可以定义出它:
1 2 3 4 5 6 7 8 9 10 11 12 13
| typedef struct sObject{ ObjectType type; union{ int value; struct { struct sObject * head; struct sObject * tail; }; }; } Object;
|
用type
字段来标识值的类型——int/pair,然后用联合体来存储
小小的虚拟机
现在我们可以在虚拟机种使用这个数据类型,我们的虚拟机拥有一个栈,用来存储当前作用域中的变量。
1 2 3 4
| typedef struct { Object * stack[STACK_MAX]; int stackSize; } VM;
|
这个结构体中包含两个部分:
- 一个是一个栈空间,用来存储栈中存储的对象的地址
- 一个是当前栈的大小
我们在此基础之上继续编写一个创建并初始化虚拟机的函数:
1 2 3 4 5
| VM * newVM(){ VM * vm = malloc(sizeof (VM)); vm->stackSize = 0; return vm; }
|
然后我们添加能对虚拟机的栈进行操作的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void push(VM * vm,Object * value){ if(vm->stackSize > STACK_MAX){ fprintf(stderr,"Stack overflow"); exit(0); } vm->stack[vm->stackSize++] = value; } Object * pop(VM * vm){ if(vm->stackSize < 0){ fprintf(stderr,"Stack underflow"); exit(0); } return vm->stack[--vm->stackSize]; }
|
我们现在可以存放变量了,那么我们也需要一个创建变量的程序:
1 2 3 4 5
| Object * newObject(VM * vm,ObjectType type){ Object * object = malloc(sizeof (Object)); object->type = type; return object; }
|
上面的辅助函数实现了对变量内存的分配,和内存类型的设定,我们在此基础上编写指定的数据类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void pushInt(VM *vm,int intValue){ Object* object = newObject(vm,OBJ_INT); object->value = intValue; push(vm,object); } Object * pushPair(VM * vm){ Object* object = newObject(vm,OBJ_PAIR);
object->tail = vm->stack[vm->stackSize-2]; object->head = vm->stack[vm->stackSize-1];
push(vm,object); return object; }
|
这就是我们的小虚拟机,如果我们真的有一个调用这些函数的解析器和解释器,那么我们手上就真的有一门语言了。如果我们有足够的内存,它甚至可以运行真正的程序。
标记
我们可以在之前的基础上开始我们的垃圾回收,第一个阶段就是标记。我们需要遍历所有可以到达的对象并设置他们的标记位,现在我们需要为之前的Object
结构设置一个标记位。且当我们创建一个新的对象时,我们修改newObject()
以初始化marked
为零。
1 2 3 4 5 6 7 8 9 10
| typedef struct sObject{ unsigned char marked; ... } Object; Object * newObject(VM * vm,ObjectType type){ Object * object = malloc(sizeof (Object)); object->type = type; object->marked = 0; return object; }
|
接下来我们开始准备标记所有可到达的函数,我们先从内存中的变量开始(即遍历栈),不过首先我们需要实现我们的标记函数,然后再遍历标记中调用:
1 2 3 4 5 6 7
| void mark(Object * object){ object->marked = 1; } void markAll(VM * vm){ for(int i=0;i<vm->stackSize;i++) mark(vm->stack[i]); }
|
不过这还远远不够,我们标记的对象本身确实是可以到达的,但是我们还有一种成对类型。在这里,我们需要意识到可达性是传递的,我们可以用递归实现这个过程:
1 2 3 4 5 6 7 8 9
| void mark(Object * object){ if(object->marked) return; object->marked = 1; if(object->type == OBJ_PAIR){ mark(object->head); mark(object->tail); } }
|
现在我们的标记已经完成了,我们可以使用markAll()
来实现对内存空间的标记
清除
现在最最最重要的一个环节到了,我们需要遍历我们分配的所有对象,并释放那些未被标记到的对象,但是这里有一个问题,按照定义,未标记的对象对我们而言是不可到达的。
VM已经实现了对对象的引用予以,所以哦我们只将对象的指针存储在变量和成对变量中。一旦对象不再被其他变量所引用,虚拟机就完全失去了它,这就发生了内存泄露。为了解决这个问题,VM需要有自己的对象引用,这些引用和用户可见的语义是不同的,换而言之,我们可以自己跟踪它们。
实现的方式也很简单,我们创建一个链表,用来加入我们分配过的所有对象,我们在Object
中完成这个过程:
1 2 3 4 5
| typedef struct sObject{ struct sObject* next; ... } Object;
|
虚拟机则负责跟踪该列表的头部:
1 2 3 4 5
| typedef struct { Object * firstObject; ... } VM;
|
在newVM()
中,我们将头指针初始化为NULL。每次创建对象时,我们都将其添加到列表中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| VM * newVM(){ VM * vm = malloc(sizeof (VM)); vm->firstObject = NULL; vm->stackSize = 0; return vm; } Object * newObject(VM * vm,ObjectType type){ Object * object = malloc(sizeof (Object)); object->type = type; object->marked = 0; object->next = vm->firstObject; vm->firstObject = object; return object; }
|
现在我们想要删除未标记的对象,只需要遍历列表即可,我们进行清除函数的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void sweep(VM * vm){ Object ** object = &vm->firstObject; while(*object){ if(!(*object)->marked){ Object * unreached = * object; *object = unreached->next; free(unreached); }else{ (*object)->marked = 0; object = &(*object)->next; } } }
|
这个代码看起来确实抽象,但是其逻辑实际上十分简单。它遍历整个链表。每当遇到未标记的对象,就释放内存,并将其从链表中移除。执行这个过程中后,我们就删除了所有不可到达的对象。(这里的二重指针真搞人心态,实际上这个object指向当前对象的next地址)。
OK至此为止,我们的垃圾回收器已经完成了,我们将其简单的合并起来:
1 2 3 4
| void GC(VM * vm){ markAll(vm); sweep(vm); }
|
但是不止如此,由于内存是有限的,我们需要设置一个对象数量的阈值,一旦超过这个数量就启动GC对内存进行垃圾回收,我们可以通过拓展VM来计算和设置数量:
1 2 3 4 5 6 7
| typedef struct { int numObjects; int maxObjects; ... } VM;
|
然后再VM的创建中初始化他们:
1 2 3 4 5 6
| VM * newVM(){ ... vm->numObjects = 0; vm->maxObjects = INITIAL_GC_THRESHOLD; return vm; }
|
其中INITIAL_GC_THRESHOLD
是第一次启动垃圾回收的对象的数量,这个可以自行调整。
当我们每创建一个对象时,我们都需要增加numObject
的数量并进行判断,当其到达最大值时进行垃圾回收:
1 2 3 4 5 6
| Object * newObject(VM * vm,ObjectType type){ ... vm->numObjects++; return object; }
|
当然每当我们使用sweep
释放一个对象,我们也应该在程序中减少当前对象的数量:
1 2 3 4 5 6 7 8 9 10 11 12
| void sweep(VM * vm){ Object ** object = &vm->firstObject; while(*object){ if(!(*object)->marked){ ... free(unreached); vm->numObjects--; }else{ ... } } }
|
最后我们需要修改GC()
来更新最大值:
1 2 3 4 5
| void GC(VM * vm){ markAll(vm); sweep(vm); vm->maxObjects = vm->numObjects*2; }
|
每次回收后我们的剩余活动对象数量会动态更新GC的触发值,如果活动对象变多,就会扩大。如果大量的活动对象被释放,就会自动缩小。
演示
本来是想演示一下这个程序怎么使用的,感觉还是算了,因为这个过程并不是很直观,所以就不在此演示了,不过如果能一直看到这里,想必整个过程始终是心里有数的了,就不在此过多概述。
好吧我还是让AI写了一个测试集:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| int main() { VM * vm = newVM();
pushInt(vm, 10); pushInt(vm, 20); pushInt(vm, 30);
Object * pair1 = pushPair(vm);
pushInt(vm, 40); pushInt(vm, 50);
Object * pair2 = pushPair(vm);
pop(vm); pop(vm);
printf("Objects before GC: %d\n", vm->numObjects);
GC(vm);
printf("Objects after GC: %d\n", vm->numObjects);
sweep(vm);
free(vm);
return 0; }
|
这是它的测试集,符合我们想要的结果。
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
| #include <stdio.h> #include <stdlib.h>
#define STACK_MAX 256 #define INITIAL_GC_THRESHOLD 10
typedef enum{ OBJ_INT, OBJ_PAIR } ObjectType;
typedef struct sObject{ struct sObject* next; unsigned char marked; ObjectType type; union{ int value;
struct { struct sObject * head; struct sObject * tail; }; }; } Object;
typedef struct { int numObjects; int maxObjects; Object * firstObject; Object * stack[STACK_MAX]; int stackSize; } VM;
VM * newVM(){ VM * vm = malloc(sizeof (VM)); vm->firstObject = NULL; vm->stackSize = 0; vm->numObjects = 0; vm->maxObjects = INITIAL_GC_THRESHOLD; return vm; }
void push(VM * vm,Object * value){ if(vm->stackSize > STACK_MAX){ fprintf(stderr,"Stack overflow"); exit(0); } vm->stack[vm->stackSize++] = value; } Object * pop(VM * vm){ if(vm->stackSize < 0){ fprintf(stderr,"Stack underflow"); exit(0); } return vm->stack[--vm->stackSize]; }
Object * newObject(VM * vm,ObjectType type){ Object * object = malloc(sizeof (Object)); object->type = type; object->marked = 0;
object->next = vm->firstObject; vm->firstObject = object;
vm->numObjects++; return object; }
void pushInt(VM *vm,int intValue){ Object* object = newObject(vm,OBJ_INT); object->value = intValue; push(vm,object); } Object * pushPair(VM * vm){ Object* object = newObject(vm,OBJ_PAIR); object->tail = vm->stack[vm->stackSize-2]; object->head = vm->stack[vm->stackSize-1];
push(vm,object); return object; }
void mark(Object * object){ if(object->marked) return; object->marked = 1; if(object->type == OBJ_PAIR){ mark(object->head); mark(object->tail); } } void markAll(VM * vm){ for(int i=0;i<vm->stackSize;i++) mark(vm->stack[i]); } void sweep(VM * vm){ Object ** object = &vm->firstObject; while(*object){ if(!(*object)->marked){ Object * unreached = * object; *object = unreached->next; free(unreached); vm->numObjects--; }else{ (*object)->marked = 0; object = &(*object)->next; } } }
void GC(VM * vm){ markAll(vm); sweep(vm); vm->maxObjects = vm->numObjects*2; }
int main(){
}
|