GC 标记 - 压缩算法(Mark Compact GC)是将 GC 标记 - 清除算法与 GC 复制算法相结合的产物。
本文实现的是Donald E. Knuth研究出来的 Lisp2 算法,基于C语言
在标记 - 整理算法中,标记阶段和标记 - 清除算法中的的标记阶段完全一样;然后对堆进行几次搜索来整理活动对象。
整理算法也是移动式的算法,下会有碎片化的问题,并且和复制算法相比不用牺牲半个堆的空间
名词解释
对象
对象在GC的世界里,代表的是数据集合,是垃圾回收的基本单位。
指针
可以理解为就是C语言中的指针(又或许是handle),GC是根据指针来搜索对象的。
mutatar
这个词有些地方翻译为赋值器,但还是比较奇怪,不如不翻译……
mutator 是 Edsger Dijkstra 琢磨出来的词,有“改变某物”的意思。说到要改变什么,那就是 GC 对象间的引用关系。不过光这么说可能大家还是不能理解,其实用一句话概括的话,它的实体就是“应用程序”。
mutatar的工作有以下两种:
生成对象
更新指针
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 marked;//是否可达
object *forwarding;//目标位置
} object;
//继承
//"继承对象"需和父对象object基本属性保持一致,在基本属性之后,可以定义其他的属性
typedef struct emp {
class_descriptor *class;//对象对应的类型
byte marked;//是否可达
object *forwarding;//目标位置
int id;
dept *dept;
} emp;
有了基本的数据结构,下面就可以进行算法的实现了
算法实现(Lisp2)
Lisp2 算法在对象头里为 forwarding 指针留出了空间。这里的forwarding 指针跟 GC 复制算法中的用法一样。
假设我们要在下面这种情况下执行 GC
标记
首先是标记阶段,标记-整理中的标记算法和标记-清除中一致;标记阶段结束后的堆状态如下图:
mark代码:
void mark(object *obj) {
if (!obj || obj->marked) { return; }
obj->marked = TRUE;
printf("marking...\n");
//递归标记对象的引用
for (int i = 0; i < obj->class->num_fields; ++i) {
mark(*((object **) ((void *) obj + obj->class->field_offsets[i])));
}
}
整理
整理阶段结束后的堆状态如下图所示。
整理代码:
void compact() {
set_forwarding();
adjust_ref();
move_obj();
}
整理阶段分为三个步骤:
计算并设置整理后的对象forwarding指针
void set_forwarding() {
int p = 0;
int forwarding_offset = 0;
//遍历堆的已使用部分,这里不用遍历全堆
//因为是顺序分配法,所以只需要遍历到已分配的终点即可
while (p < next_free_offset) {
object *obj = (object *) (p + heap);
//为可达的对象设置forwarding
if (obj->marked) {
obj->forwarding = (object *) (forwarding_offset + heap);
forwarding_offset = forwarding_offset + obj->class->size;
}
p = p + obj->class->size;
}
}
调整对象的引用为移动后的地址
如上图所示,调整引用后,gc roots和其他对象的引用都已经更新为了预先计算的forwarding指针
void adjust_ref() {
int p = 0;
//先将roots的引用更新
for (int i = 0; i < _rp; ++i) {
object *r_obj = _roots[i];
_roots[i] = r_obj->forwarding;
}
//再遍历堆,更新存活对象的引用
while (p < next_free_offset) {
object *obj = (object *) (p + heap);
if (obj->marked) {
//更新引用为forwarding
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;
}
}
移动对象
void move_obj() {
int p = 0;
int new_next_free_offset = 0;
while (p < next_free_offset) {
object *obj = (object *) (p + heap);
if (obj->marked) {
//移动对象至forwarding
obj->marked = FALSE;
memcpy(obj->forwarding, obj, obj->class->size);
new_next_free_offset = new_next_free_offset + obj->class->size;
}
p = p + obj->class->size;
}
//清空移动后的间隙
memset((void *)(new_next_free_offset+heap),0,next_free_offset-new_next_free_offset);
//移动完成后,更新free pointer为新的边界指针
next_free_offset = new_next_free_offset;
}
通过上图我们能够确认,整理后,活动对象 B、C、D、F 分别对应整理后的BꞋ 、CꞋ、DꞋ 、FꞋ 。在 Lisp2 算法中,整理阶段并不会改变对象的排列顺序,只是缩小了它们之间的空隙,把它们聚集到了堆的一端。
以上就是对标记-整理算法的说明
优点
没有碎片化问题,而且可以利用整个堆,不用像复制算法那样将堆一分为二
缺点
整理成本过高,在上述实现中,对堆进行了3次搜索。也就是说该算法的时间花费是和堆大小成正比的,和存活对象数量无关
完整代码
相关文章
参考
《垃圾回收的算法与实现》 中村成洋 , 相川光 , 竹内郁雄 (作者) 丁灵 (译者)
《垃圾回收算法手册 自动内存管理的艺术》 理查德·琼斯 著,王雅光 译