GC 标记 - 压缩算法(Mark Compact GC)是将 GC 标记 - 清除算法与 GC 复制算法相结合的产物。
本文实现的是Donald E. Knuth研究出来的 Lisp2 算法,基于C语言git
在标记 - 整理算法中,标记阶段和标记 - 清除算法中的的标记阶段彻底同样;而后对堆进行几回搜索来整理活动对象。github
整理算法也是移动式的算法,不会有碎片化的问题,而且和复制算法相比不用牺牲半个堆的空间算法
对象在GC的世界里,表明的是数据集合,是垃圾回收的基本单位。segmentfault
能够理解为就是C语言中的指针(又或许是handle),GC是根据指针来搜索对象的。数据结构
这个词有些地方翻译为赋值器,但仍是比较奇怪,不如不翻译……spa
mutator 是 Edsger Dijkstra 琢磨出来的词,有“改变某物”的意思。说到要改变什么,那就是 GC 对象间的引用关系。不过光这么说可能你们仍是不能理解,其实用一句话归纳的话,它的实体就是“应用程序”。
mutatar的工做有如下两种:翻译
mutator 在进行这些操做时,会同时为应用程序的用户进行一些处理(数值计算、浏览网页、编辑文章等)。随着这些处理的逐步推动,对象间的引用关系也会“改变”。伴随这些变化会产生垃圾,而负责回收这些垃圾的机制就是 GC。
GC ROOTS就是引用的起始点,好比栈,全局变量设计
堆就是进程中的一段动态内存,在GC的世界里,通常会先申请一大段堆内存,而后mutatar在这一大段内存中进行分配3d
活动对象就是能经过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 算法在对象头里为 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(); }
整理阶段分为三个步骤:
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次搜索。也就是说该算法的时间花费是和堆大小成正比的,和存活对象数量无关
https://github.com/kongwu-/gc_impl/tree/master/mark-compact