递归调用的次数统计c语言,垃圾回收算法实现之 - 引用计数(完整可运行C语言代码)...

GC 原本是一种“释放怎么都无法被引用的对象的机制”。那么人们自然而然地就会

想到,可以让所有对象事先记录下“有多少程序引用自己”。让各对象知道自己的“人气指 数”,从而让没有人气的对象自己消失,这就是引用计数法(Reference Counting),它是 George E. Collins

于 1960 年钻研出来的。

引用计数法中引入了一个概念,那就是“计数器”。在对象头中增加了一个计数器属性,用来标识对象的被引用数量,也就是有多少程序引用了这个对象。

本文代码使用C语言实现

名词解释

对象

对象在GC的世界里,代表的是数据集合,是垃圾回收的基本单位。

指针

可以理解为就是C语言中的指针(又或许是handle),GC是根据指针来搜索对象的。

mutator

这个词有些地方翻译为赋值器,但还是比较奇怪,不如不翻译……mutator 是 Edsger Dijkstra 琢磨出来的词,有“改变某物”的意思。说到要改变什么,那就是 GC 对象间的引用关系。不过光这么说可能大家还是不能理解,其实用一句话概括的话,它的实体就是“应用程序”。

mutator的工作有以下两种:生成对象

更新指针mutator 在进行这些操作时,会同时为应用程序的用户进行一些处理(数值计算、浏览网页、编辑文章等)。随着这些处理的逐步推进,对象间的引用关系也会“改变”。伴随这些变化会产生垃圾,而负责回收这些垃圾的机制就是 GC。

GC ROOTS

GC ROOTS就是引用的起始点,比如栈,全局变量

堆(Heap)

堆就是进程中的一段动态内存,在GC的世界里,一般会先申请一大段堆内存,然后mutatar在这一大段内存中进行分配

1460000022062600

活动对象和非活动对象

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

准备工作

在引用计数算法中,使用空闲链表(free-list)的内存分配策略

空闲链表(free-list)内存分配

空闲链表分配使用某种数据结构(一般是链表)来记录空闲内存单元的位置和大小,该数据结构即为空闲内存单元的集合。

在需要分配内存时,顺序遍历每一个内存单元,找到第一个空闲的内存单元使用。

在本文中,为了降低复杂度,只使用了最基本的free-list分配法,free-list数据结构如下图所示:

1460000022062601

为了实现简单,在本文代码中,每个单元只存储一个对象,不考虑单元拆分合并等问题。

数据结构设计

首先是对象类型的结构:

为了动态访问“对象”的属性,此处使用属性偏移量来记录属性的位置,然后通过指针的计算获得属性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;//对象对应的类型

int ref_cnt;//对象被引用的次数,"人气"

} object;

//继承

//"继承对象"需和父对象object基本属性保持一致,在基本属性之后,可以定义其他的属性

typedef struct emp {

class_descriptor *class;//对象对应的类型

int ref_cnt;//对象被引用的次数,"人气"

int id;

dept *dept;

} emp;

free-list结构设计struct _node {

node *next;

byte used;//是否使用

int size;//单元大小

object *data;//单元中的数据

};

有了基本的数据结构,下面就可以进行算法的实现了,以下执行GC前堆的状态图:

1460000022062602

算法实现

在其他回收算法中,没有空闲内存分配时会调用GC,回收那些已经时垃圾的对象内存。

然而在引用计数算法中并没有明确启动GC的地方。引用计数算法与mutator的执行关联性强,在mutator的处理过程中通过计数器的更新来进行内存管理;算是一种“实时”垃圾回收算法

引用计数算法中,有两种情况会更新对象的计数器,分别是创建对象/更新对象引用

创建对象&内存分配

和标记-清除算法一样,需要先找到空闲的内存单元node *find_idle_node() {

for (next_free = head; next_free && next_free->used; next_free = next_free->next) {}

//还找不到就触发回收

if (!next_free) {

gc();

}

for (next_free = head->next; next_free && next_free->used; next_free = next_free->next) {}

//再找不到真的没了……

if (!next_free) {

printf("Allocation Failed!OutOfMemory...\n");

abort();

}

}

然后在找到的空闲内存单元中分配新对象,并初始化object *gc_alloc(class_descriptor *class) {

if (!next_free || next_free->used) {

find_idle_node();

}

//赋值当前freePoint

node *_node = next_free;

//新分配的对象指针

//将新对象分配在free-list的节点数据之后,node单元的空间内除了sizeof(node),剩下的地址空间都用于存储对象

object *new_obj = (void *) _node + sizeof(node);

new_obj->class = class;

//初始化计数器

new_obj->ref_cnt = 0;

_node->used = TRUE;

_node->data = new_obj;

_node->size = class->size;

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;

}

next_free = next_free->next;

return new_obj;

}

更新对象引用

更新对象引用,就是将对象引用的对象将A更新为B,obj->ref_a = b/**

* 修改引用

* @param ptr 原指针,这个指针是引用的指针,pointer of pointer

* @param obj 新对象指针

*/

void gc_update_ptr(object **ptr, void *obj) {

inc_ref_cnt(obj);

dec_ref_cnt(*ptr);

*ptr = obj;

}

虽然在 mutator 更新指针时程序会执行此函数,但事实上进行指针更新的只有最后一哈昂行的 *ptr = obj 部分,其他是进行内存管理的代码

inc_ref_cnt是对指针 ptr 新引用的对象(obj)的计数器进行增加操作void inc_ref_cnt(object *obj) {

if (!obj) {

return;

}

obj->ref_cnt++;

}

dec_ref_cnt是对指针 ptr 之前引用的对象(*ptr)的计数器进行减少操作void dec_ref_cnt(object *obj) {

if (!obj) {

return;

}

obj->ref_cnt--;

//如果计数器为0,则对象需要被回收,那么该对象引用的对象计数器都需要减少

if (obj->ref_cnt == 0) {

for (int i = 0; i < obj->class->num_fields; ++i) {

dec_ref_cnt(*((object **) ((void *) obj + obj->class->field_offsets[i])));

}

//回收

reclaim(obj);

}

}

在dec_ref_cnt方法中,首先对引用指针原有的引用对象计数器进行减少的操作。如果计数器减少后为0,则该对象不可达了,没有任何引用成了垃圾,需要被回收。

因为对象即将被回收,所以需要对这个对象所有的引用对象计数器也进行减少操作,并递归执行该逻辑。

以上就是对引用计数算法的说明

优点

可以及时回收垃圾,当对象的计数器为0时,对象就会被回收;由于单次回收的对象单一,所以mutator需要暂停的时间会很短,对应用造成的影响比较小;在此算法中不用遍历对象图来查找存活对象

缺点

每次对象关系变化,都需要更新计数器,更新过于频繁;处理循环引用时较为麻烦(有些资料上说引用计数无法处理循环引用不太严谨,结合部分标记-清除算法就可以解决此问题)

循环引用的例子:

1460000022063257

上图中,两个对象互相引用,计数器都为1;但对于GC ROOT都是不可达的,实际上应该是两个非存活对象,但由于互相引用,所以也会无法回收

完整代码

相关文章

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值