看了一大堆的GC算法理论,是不是还是觉得差点什么呢?那就跟着本文的思路,自己动手写个”年轻代回收算法“ - 复制算法 试试吧
GC 复制算法(Copying GC)是 Marvin L. Minsky 在 1963 年研究出来的算法。说得简单点,就是只把某个空间里的活动对象复制到其他空间,把原空间里的所有对象都回收掉。这是一个相当大胆的算法。在此,我们将复制活动对象的原空间称为 From 空间,将粘贴活动对象的新空间称为 To 空间。
本文内容主要参考《垃圾回收的算法与实现》 ,使用 C 语言实现;文末附有源码地址,完整可运行
名词解释
对象
对象在 GC 的世界里,代表的是数据集合,是垃圾回收的基本单位。
指针
可以理解为就是 C 语言中的指针(又或许是 handle),GC 是根据指针来搜索对象的。
mutator
这个词有些地方翻译为赋值器,但还是比较奇怪,不如不翻译……
mutator 是 Edsger Dijkstra 琢磨出来的词,有 “改变某物” 的意思。说到要改变什么,那就是 GC 对象间的引用关系。不过光这么说可能大家还是不能理解,其实用一句话概括的话,它的实体就是“应用程序”。
mutator 的工作有以下两种:
- 生成对象
- 更新指针
mutator 在进行这些操作时,会同时为应用程序的用户进行一些处理(数值计算、浏览网页、编辑文章等)。随着这些处理的逐步推进,对象间的引用关系也会 “改变”。伴随这些变化会产生垃圾,而负责回收这些垃圾的机制就是 GC。
GC ROOTS
GC ROOTS 就是引用的起始点,比如栈,全局变量
堆 (Heap)
堆就是进程中的一段动态内存,在 GC 的世界里,一般会先申请一大段堆内存,然后 mutatar 在这一大段内存中进行分配
活动对象和非活动对象
活动对象就是能通过 mutatar(GC ROOTS)引用的对象,反之访问不到的就是非活动对象。
准备工作
在复制算法中,使用顺序内存分配 (sequential allocation) 策略,顺序分配流程如下图所示
维护一个 free pointer,每次分配内存后移动该指针,limit-free 的就是当前堆中可用内存的大小
数据结构设计
首先是对象类型的结构:
为了动态访问 “对象” 的属性,此处使用属性偏移量来记录属性的位置,然后通过指针的计算获得属性
typedef struct class_descriptor {
char *name;//类名称
int size;//类大小,即对应sizeof(struct)
int num_fields;//属性数量
int *field_offsets;//类中的属性偏移,即所有属性在struct中的偏移量
} class_descriptor;
然后是对象的结构,虽然 C 语言中没有继承的概念,但是可以通过共同属性的 struct 来实现:
typedef struct _object {
class_descriptor *class;//对象对应的类型
byte forwarded;//对象已经移动的标记,防止被重复复制
object *forwarding;//目标位置
} object;
//继承
//"继承对象"需和父对象object基本属性保持一致,在基本属性之后,可以定义其他的属性
typedef struct emp {
class_descriptor *class;//对象对应的类型
byte forwarded;//对象已经移动的标记
object *forwarding;//目标位置
int id;
dept *dept;
} emp;
有了基本的数据结构,下面就可以进行算法的实现了
算法实现
复制算法利用 From 空间进行分配。当 From 空间被完全占满无法分配时,GC 会将活动对象全部复制到 To 空间。当复制完成后,会将 From/To 空间互换,为下次 GC 做准备。在本算法中,为了确保 To 空间可以容纳所有 From 空间的活动对象,需要 From 和 To 空间容量保持一致。
复制算法的流程如下图所示:
初始化堆
复制算法中,需要将堆一分为二,一半作为 from,一半作为 to
void gc_init(int size) {
heap_size = resolve_heap_size(size);
heap_half_size = heap_size / 2;
heap = (void *) malloc(heap_size);
from = heap;
to = (void *) (heap_half_size + from);
_rp = 0;
}
创建对象 & 内存分配
新创建对象分配内存时,只需要移动 free pointer 即可
next_free_offset 就是图中的 free pointer
object *gc_alloc(class_descriptor *class) {
//检查是否可以分配
if (next_free_offset + class->size > heap_half_size) {
printf("Allocation Failed. execute gc...\n");
gc();
if (next_free_offset + class->size > heap_half_size) {
printf("Allocation Failed! OutOfMemory...\n");
abort();
}
}
int old_offset = next_free_offset;
//分配后,free移动至下一个可分配位置
next_free_offset = next_free_offset + class->size;
//分配
object *new_obj = (object *) (old_offset + heap);
//初始化
new_obj->class = class;
new_obj->forwarded = FALSE;
new_obj->forwarding = NULL;
for (int i = 0; i < new_obj->class->num_fields; ++i) {
//*(data **)是一个dereference操作,拿到field的pointer
//(void *)o是强转为void* pointer,void*进行加法运算的时候就不会按类型增加地址
*(object **) ((void *) new_obj + new_obj->class->field_offsets[i]) = NULL;
}
return new_obj;
}
复制
复制时,需从 GC ROOTS 开始遍历对象图,对每一个存活的对象进行复制;复制后对象地址改变,还需要更新 GC ROOTS 引用的地址;
void copying() {
next_forwarding_offset = 0;
//遍历GC ROOTS
for (int i = 0; i < _rp; ++i) {
object *forwarded = copy(_roots[i]);
//先将GC ROOTS引用的对象更新到to空间的新对象
_roots[i] = forwarded;
}
//更新引用
adjust_ref();
//清空from,并交换from/to
swap(&from,&to);
}
复制算法流程如下:
copy 方法:
object *copy(object *obj) {
if (!obj) { return NULL; }
//由于一个对象可能会被多个对象引用,所以此处判断,避免重复复制
if (!obj->forwarded) {
//计算复制后的指针
object *forwarding = (object *) (next_forwarding_offset + to);
//赋值
memcpy(forwarding, obj, obj->class->size);
obj->forwarded = TRUE;
//将复制后的指针,写入原对象的forwarding pointer,为最后更新引用做准备
obj->forwarding = forwarding;
//复制后,移动to区forwarding偏移
next_forwarding_offset += obj->class->size;
//递归复制引用对象,递归是深度优先
for (int i = 0; i < obj->class->num_fields; i++) {
copy(*(object **) ((void *) obj + obj->class->field_offsets[i]));
}
return forwarding;
}
return obj->forwarding;
}
Forwarding pointer
个人觉得 “转发指针(Forwarding Pointer)” 在复制算法中还是一个比较重要的概念
转发指针,指的时复制时,在原对象里保留新对象的指针。为什么要保留这个指针呢?
因为需要复制的不只是对象,对象的引用关系也需要复制。比如下图,对象 ACD 都需要复制,且只复制了对象 A 时,实际上复制的对象 A'(一撇)引用的 CD 还是未复制的
调整引用
在所有活动对象都复制完毕后,需要将引用的地址调整为复制后的对象地址;只需要遍历一边 to 空间,找到引用对象的 forwarding pointer 更新即可
void adjust_ref() {
int p = 0;
//遍历to,即复制的目标空间
while (p < next_forwarding_offset) {
object *obj = (object *) (p + to);
//将还指向from的引用更新为forwarding pointer,即to中的pointer
for (int i = 0; i < obj->class->num_fields; i++) {
object **field = (object **) ((void *) obj + obj->class->field_offsets[i]);
if ((*field) && (*field)->forwarding) {
*field = (*field)->forwarding;
}
}
//顺序访问下一个对象
p = p + obj->class->size;
}
}
以上就是对复制算法的说明
优点
- 吞吐量高,不需要遍历全堆,只需要处理活动对象
- 分配速度快,和 free-list 分配法相比,顺序分配不需要搜索 free-list,只需要移动 free pointer 即可
- 不会有碎片化的问题,因为每次复制都将存活对象从 from 复制到 to 的一端
缺点
堆利用率较低,因为在复制算法下,只有一半的内存用来存储对象。
写在后面
本文介绍的复制算法,是最基础的复制算法。像JVM里的复制算法是后来的改进版,后面介绍分代回收算法的时候会详细介绍
完整代码
参考
- 《垃圾回收的算法与实现》 中村成洋 , 相川光 , 竹内郁雄 (作者) 丁灵 (译者)
- 《垃圾回收算法手册 自动内存管理的艺术》 理查德 · 琼斯 著,王雅光 译
原创不易,转载请在开头著名文章来源和作者。如果我的文章对您有帮助,请点赞/收藏/关注鼓励支持一下吧❤❤❤❤❤❤