GC 本来是一种“释放怎么都没法被引用的对象的机制”。那么人们天然而然地就会
想到,可让全部对象事先记录下“有多少程序引用本身”。让各对象知道本身的“人气指 数”,从而让没有人气的对象本身消失,这就是引用计数法(Reference Counting),它是 George E. Collins
于 1960 年钻研出来的。
引用计数法中引入了一个概念,那就是“计数器”。在对象头中增长了一个计数器属性,用来标识对象的被引用数量,也就是有多少程序引用了这个对象。node
本文代码使用C语言实现git
对象在GC的世界里,表明的是数据集合,是垃圾回收的基本单位。github
能够理解为就是C语言中的指针(又或许是handle),GC是根据指针来搜索对象的。算法
这个词有些地方翻译为赋值器,但仍是比较奇怪,不如不翻译……segmentfault
mutator 是 Edsger Dijkstra 琢磨出来的词,有“改变某物”的意思。说到要改变什么,那就是 GC 对象间的引用关系。不过光这么说可能你们仍是不能理解,其实用一句话归纳的话,它的实体就是“应用程序”。
mutatar的工做有如下两种:数据结构
mutator 在进行这些操做时,会同时为应用程序的用户进行一些处理(数值计算、浏览网页、编辑文章等)。随着这些处理的逐步推动,对象间的引用关系也会“改变”。伴随这些变化会产生垃圾,而负责回收这些垃圾的机制就是 GC。
GC ROOTS就是引用的起始点,好比栈,全局变量函数
堆就是进程中的一段动态内存,在GC的世界里,通常会先申请一大段堆内存,而后mutatar在这一大段内存中进行分配spa
活动对象就是能经过mutatar(GC ROOTS)引用的对象,反之访问不到的就是非活动对象。翻译
在引用计数算法中,使用空闲链表(free-list)的内存分配策略设计
空闲链表分配使用某种数据结构(通常是链表)来记录空闲内存单元的位置和大小,该数据结构即为空闲内存单元的集合。
在须要分配内存时,顺序遍历每个内存单元,找到第一个空闲的内存单元使用。
在本文中,为了下降复杂度,只使用了最基本的free-list分配法,free-list数据结构以下图所示:
为了实现简单,在本文代码中,每一个单元只存储一个对象,不考虑单元拆分合并等问题。
首先是对象类型的结构:
为了动态访问“对象”的属性,此处使用属性偏移量来记录属性的位置,而后经过指针的计算得到属性
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前堆的状态图:
在其余回收算法中,没有空闲内存分配时会调用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须要暂停的时间会很短,对应用形成的影响比较小;在此算法中不用遍历对象图来查找存活对象
每次对象关系变化,都须要更新计数器,更新过于频繁;处理循环引用时较为麻烦(有些资料上说引用计数没法处理循环引用不太严谨,结合部分标记-清除算法就能够解决此问题)
循环引用的例子:
上图中,两个对象互相引用,计数器都为1;但对于GC ROOT都是不可达的,实际上应该是两个非存活对象,但因为互相引用,因此也会没法回收
https://github.com/kongwu-/gc_impl/tree/master/reference-counting