c语言剪切复制粘贴算法,垃圾回收算法实现之 - 标记-整理(完整可运行C语言代码)...

本文详细介绍了Donald E. Knuth提出的Lisp2算法,该算法结合了标记-清除和复制算法的特点,用于解决垃圾回收问题。在C语言环境中,实现了标记-整理算法,包括标记阶段和整理阶段。标记阶段与标记-清除相同,而整理阶段则通过移动对象并更新引用来消除碎片,确保堆内存的有效利用。虽然该算法避免了碎片,但其整理过程代价较高,需要对堆进行多次扫描。
摘要由CSDN通过智能技术生成

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在这一大段内存中进行分配

202003191504528303zfn6w97ydohl56_5.png

活动对象和非活动对象

活动对象就是能通过mutatar(GC ROOTS)引用的对象,反之访问不到的就是非活动对象。

准备工作

在标记-整理算法中,使用顺序内存分配(sequential allocation)策略,顺序分配流程如下图所示

202003191504528303zfn6w97ydohl56_8.png

维护一个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

202003191504528303zfn6w97ydohl56_4.png

标记

首先是标记阶段,标记-整理中的标记算法和标记-清除中一致;标记阶段结束后的堆状态如下图:

202003191504528303zfn6w97ydohl56_7.png

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])));

}

}

整理

整理阶段结束后的堆状态如下图所示。

202003191504528303zfn6w97ydohl56_6.png

整理代码:

void compact() {

set_forwarding();

adjust_ref();

move_obj();

}

整理阶段分为三个步骤:

计算并设置整理后的对象forwarding指针

202003191504528303zfn6w97ydohl56_2.png

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;

}

}

调整对象的引用为移动后的地址

202003191504528303zfn6w97ydohl56_1.png

如上图所示,调整引用后,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;

}

}

移动对象

202003191504528303zfn6w97ydohl56_3.png

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次搜索。也就是说该算法的时间花费是和堆大小成正比的,和存活对象数量无关

完整代码

相关文章

参考

《垃圾回收的算法与实现》 中村成洋 , 相川光 , 竹内郁雄 (作者) 丁灵 (译者)

《垃圾回收算法手册 自动内存管理的艺术》 理查德·琼斯 著,王雅光 译

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值