目录算法
George E.Collins.1960.数组
引用计数算法中引入了一个概念计数器。计数器表明对象被引用的次数,它是无符号正整数用于计数器的位数根据算法和实现有所不一样。本文结构以下图所示。缓存
在变动数组元素等的时候会进行指针的更新。经过更新指针,可能会产生没有被任何程序引用的垃圾对象。引用计数法中会监督在更新指针的时候是否有产生垃圾,从而在产生 垃圾时将其马上回收。这样,能够说将内存管理和 mutator 同时运行正是引用计数法的一大特征。分布式
引用计数算法中,mutator没有明确启动GC的语句。它在mutator的处理过程当中经过增减计数器的值来进行内存管理,这两种状况下计数器的值会发生改变,这使用到了new_obj()函数和update_ptr()函数。函数
new_obj(size){ obj = pickup_chunk(size, $free_list) if(obj == NULL) allocation_fail() else obj.ref_cnt = 1 return obj }
这是生成新的对象,使用pickup_chunk()函数。当函数返回NULL是表明分配失败。在引用计数法中,除了链接到空闲链表的对象,其余全部对象都是活动对象。也就是说,一旦返回NULL就表明没有任何多余的空间了。也就没法进行分配。指针
当pickup_chunk()函数返回合适大小的对象时,讲他的计数器ref_cnt置为1,表明其被引用了1次。code
update_ptr(ptr, obj){ inc_ref_cnt(obj) //计数器+ dec_ref_cnt(*ptr) //计数器- *ptr = obj // 从新指向 obj }
在mutator更新指针时会执行此程序。其、*ptr = obj是指针更新的部分。orm
inc_ref_cnt(obj)对指针ptr新引用对象obj计数器进行增量操做。对象
inc_ref_cnt(obj){ obj.ref_cnt++ }
仅仅是对对象计数器进行自增操做。blog
dec_ref_cnt(*ptr)对以前ptr引用的对象进行计数器减量操做。
dec_ref_cnt(*ptr){ obj.ref_cnt-- if (obj.ref_cnt == 0) for(child : children(obj)) // 当本身被清除时,本身所引用的孩子的计数器必须减一。进行递归操做。 dec_ref_cnt(*child) reclaim(obj) }
疑问为何要先inc_ref_cnt(obj)而后再dec_ref_cnt(*ptr)呢?
以为应该先调用 dec_ref_cnt() 函数,后调 用 inc_ref_cnt() 函数才合适。
答案就是“为了处理ptr和obj是同一对象时的状况”。若是按照先dec_ref_cnt()后inc_ref_cnt()函数的顺序调用ptr和 obj又是同一对象的话执行dec_ref_cnt(ptr)时ptr的计数器的值就有可能变为0而被回收。这样一来,下面再想执行inc_ref_cnt(obj)时obj早就被回收了,可能会引起重大的BUG。
初始状态下从根引用A和C,从A引用B。A持有惟一指向B的指针(代指状态a),假设如今将A指针更新到了C(状态b)。以下图。
B的计数器值变成了0,所以B被回收。且B链接上了空闲链表,可以再被利用了。又由于新造成了由A指向C的指针,因此C的计数器的值增量为2。
在引用计数法中,每一个对象始终都知道本身的被引用数(就是计数器的值)。当被引用数的值为0时,对象立刻就会把本身做为空闲空间链接到空闲链表。也就是说,各个对象在变成垃圾的同时就会马上被回收。要说这有什么意义,那就是内存空间不会被垃圾占领。垃圾 所有都已链接到空闲链表,能做为分块再被利用。
在引用计数法中,只有当经过mutator更新指针时程序才会执行垃圾回收。也就是说,每次经过执行mutator 生成垃圾时这部分垃圾都会被回收,于是大幅度地削减了mutator的最大暂停时间。
引用计数法和GC标记-清除算法不同,不必由根沿指针查找。当咱们想减小沿指针查找的次数时,它就派上用场了。 打个比方,在分布式环境中,若是要沿各个计算节点之间的指针进行查找,成本就会增大,所以须要极力控制沿指针查找的次数。 因此有一种作法是在各个计算节点内回收垃圾时使用GC标记-清除算法在考虑到节点间的引用关系时则采用引用计数法。
在大多数状况下指针都会频繁地更新,特别是有根的指针,会以近乎使人目眩的势头飞速地进行更新。这是由于根能够经过mutator直接被引用。在引用计数法中,每当指针更新时,计数器的都会随之更新,所以值的增减处理必然会变得繁重。
用于引用计数的计数器最大必须能数完堆中全部对象的引用数。打个比方,假如咱们用的是32位机器,那么就有可能要让2 的32次方个对象同时引用一个对象。考虑到这种状况,就有必要确保各对象的计数器有32位大小。也就是说,对于全部对象,必须留有32位的空间。这就害得内存空间的使用效率大大下降了。打比方说,假如对象只有2个域,那么其计数器就占了它总体的1/3。
进行指针更新操做 update_ptr()函数是在mutator这边调用的打个比方咱们须要把以往写成*ptr=obj的地方都重写成updat_ptr(ptr,obj)。由于调用update_ptr()函数的地方很是多,因此重写过程当中很容易出现遗漏。若是漏掉了某处,内存管理就没法正确进行,就会产生BUG。
由于两个对象互相引用,因此各对象的计数器的值都是1。可是这些对象组并无被其余任何对象引用。所以想一并回收这两个对象都不行,只要它们的计数器值都是1,就没法回收。
在讲到引用计数法缺点时候,咱们提到了计数器增减处理繁重。下面就对改善此缺点进行说明即延迟引用计数法(Deferred Reference Countin [L. Peter Deutsch 和 Daniel G. Bobrow] )。
咱们就让从根引用的指针的变化不反映在计数器上。打个比方,咱们把重写全局变量指针的 update_ptr(ptr,obj) 改写成 *ptr = obj(直接更改引用对象,没有使用方法也就没有对计数器进行操做。)。如上所述,这样一来即便频繁重写堆中对象的引用关系,对象的计数器值也不会有所变化,于是大大改善了“计数器值的增减处理繁重”这一缺点。然而,这样内存管理仍是不能顺利进行。由于引用没有反映到计数器上,因此各个对象的计数器没有正确表示出对象自己的被引用数。所以,有可能发生对象仍在活动,但却被错当成垃圾回收的状况。 以下图所示该对象其实还正在活动。
以后咱们在延迟计数法中使用ZCT(Zero Count Table)。ZTC是一个表,他会事先记录下计数器值在dec_ref_cnt()函数的做用下变为0的对象。
由于计数器值为0的对象不必定都是垃圾,因此暂时先将这些对象保留。由图也能看出,咱们必须修正dec_ref_cnt() 函数,使其适应延迟引用计数法。
dec_ref_cnt(obj){ obj.ref_cnt-- if(obj.ref_cnt == 0) if(is_full($zct) == TRUE) scan_zct() push($zct, obj) }
当obj计数器为0,就把obj添加到$zct中。不过当$zct爆满,首先要经过scan_zct()函数来减小$zct中的对象。
也要修改一线new_obj()函数。当没法分配空间时,执行scan_zct()清理一遍$zct对象(表)。
new_obj(size){ obj = pickup_chunk(size, $free_list) if(obj == NULL) scan_zct() obj = pickup_chunk(size, $free_list) if(obj == NULL) allocation_fall() obj.ref_cnt = 1 return obj }
若是第一次分配没有顺利进行,就意味着空闲链表中没有了大小合适的分块。此时程序要搜索一遍$zct,以再次分配分块。若是这样还不行,分配就失败了。分配顺利进行以后的流程一般与引用计数法彻底同样。
scan_zct(){ for(r :$roots) // 以前咱们说过,把根的引用不反应在计数器上,如今要清表了天然要加上。 (*r).ref_cnt++ for(obj :$zct) // 查水表 if(obj.ref_cnt == 0) // 判断值 remove($zct, obj) // 减计数 delete(obj) //清对象 for(r :$root) (*r).ref_cnt-- // 用完了在给根加回去。 }
// delete代码清单 delete(obj){ for(child :children(obj)) (*child).ref_cnt-- if((*child).ref_cnt == 0) delete(*child) // 一样要递归的去操做孩子的计数器 reclaim(obj) // 加到空闲链表上 }
优势在延迟引用计数法中,程序延迟了根引用的计数,将垃圾一并回收。经过延迟,减轻了 因根引用频繁发生变化而致使的计数器增减所带来的额外负担。
缺点为了延迟计数器值的增减,垃圾不能立刻获得回收,这样一来垃圾就会压迫堆,咱们也 就失去了引用计数法的一大优势(即刻回收垃圾)。
缺点scan_zct() 函数致使最大暂停时间延长了,执行scan_zct() 函数所花费的时间 $zct的大小成正比。$zct 越大,要搜索的对象就越多,妨碍mutator运做的时间也就越长。要想缩减因scan_zct()函数而致使的暂停时间,就要缩小 $zct。可是这样一来调用scan_ zct()函数的频率就增长了,也压低了吞吐量。很明显这样就本末倒置了。
在引用计数法中,咱们有必要花功夫来研究一件事,那就是要为计数器设置多大的位宽。 假设为了反映全部引用,计数器须要1个字(32位机器就是32位)的空间。可是这样会大量消耗内存空间。打个比方,2 个字的对象就要附加1个字的计数器。也就是说,计数器害得对象所占空间增大了1.5 倍。
对此咱们有个方法,那就是用来减小计数器位宽的“Sticky引用计数法”。举个例子咱们假设用于计数器的位数为5位,那么这种计数器最多只能数到2的5次方减1也就是31个引用数。若是此对象被大于31个对象引用,那么计数器就会溢出。这跟车辆速度计的指针爆表是一个情况。
针对计数器溢出(也就是爆表的对象),须要暂停对计数器的管理。对付这种对象,咱们主要有两种方法。
对于计数器溢出的对象,咱们能够这样处理:再也不增减计数器的值,就把它放着,什么也不作。不过这样一来,即便这个对象成了垃圾(即被引用数为 0),也不能将其回收。也就是说,白白浪费了内存空间。然而事实上有不少研究代表,不少对象一辈子成立刻就死了。也就是说, 在不少状况下,计数器的值会在0到1的范围内变化,鲜少出现 5 位计数器溢出这样的状况。 此外,由于计数器溢出的对象在执行中的程序里占有很是重要的地位,因此可想而知,其将 来成为垃圾的可能性也很低。也就是说,不增减计数器的值,就把它那么放着也不会有什么大问题。 考虑到以上事项,对于计数器溢出的对象,什么也不作也不失为一个可用的方法。
另外一个方法是,能够使用标记清除算法来辅助。可是须要对标记清除进行修改。
mark_sweep_for_counter_overflow()
// mark_sweep_for_counter_overflow() mark_sweep_for_counter_overflow(){ reset_all_ref_cnt() mark_phase() sweep_phase() }
首先把全部对象的计数器都置为0。下面进入标记阶段和清除阶段。
mark_phase()
// mark_phase() mark_phase(){ for(r :roots) push(*r, $mark_stack) while(is_empty($mark_stack) == FALSE) obj = pop($mark_stack) obj.ref_cnt++ if(obj.ref_cnt == 1) for(child :children(obj)) push(*child, $mark_stack) }
首先把由根直接引用的对象堆到标记栈里,而后按顺序从标记栈取出对象。对计数器进行增量操做。不过这里必须把各个对象及其子对象堆进标记栈一次。while里的if是检测各个对象是否是只进栈一次。一旦栈为空,则标记阶段结束。
sweep_phase()
// sweep_phase() sweep_phase(){ sweeping = $heap_top while(sweeping < $heap_end) if(sweeping.ref_cnt == 0) reclaim(sweeping) sweeping +=sweeping.size }
在清除阶段,程序会搜索整个堆,回收计数器值仍为0的对象。
这里的标记清除,和以前的标记清除主要有如下3点不一样之处。
像这样,只要把引用计数法和 GC 标记 - 清除算法结合起来,在计数器溢出后即便对象 成了垃圾,程序仍是能回收它。另外还有一个优势,那就是还能回收循环的垃圾。
可是在进行标记处理以前,必须重置全部的对象和计数器。此外,由于在查找对象时没 有设置标志位而是把计数器进行增量,因此须要屡次(次数和被引用数一致)查找活动对象。 考虑到这一点的话,显然在这里进行的标记处理比以往的 GC 标记 - 清除算法中的标记处理 要花更多的时间。也就是说,吞吐量会相应缩小。
1位引用计数法(1bit Reference Counting)是Sticky引用计数法的一个极端例子。由于计数器只有1位大小,因此瞬间就会溢出。
据Douglas W. Clark和C. Cordell Green观察,“几乎没有对象是被共有的,全部对象都能被立刻回收”。考虑到这一点,即便计数器只有 1 位,经过用 0表示被引用数为1 ,用1表示被引用数大于等于2,这样也能有效率地进行内存管理。以下图示。
咱们用1位来表示某个对象的被引用数是1个仍是多个。通常引用计数法是让对象持有计数器,可是W.R.Stoye、T.J.W.Clarke、A.C.Norman 3我的想出了1位引用计数法,以此来让指针持有计数器。
设对象引用数为1时标签位为0,引用数为复数时标签位为1。咱们分别称以上2种状态为UNIQUE和MULTIPLE,处于UNIQUE状态下的指针为“UNIQUE指针”,处于MULTIPLE状态下的指针为“MULTIPLE 指针”。
1 位引用计数法也是在更新指针的时候进行内存管理的。不过它不像以往那样 指定要引用的对象来更新指针,而是经过复制某个指针来更新指针。进行这项操做的就是 copy_ptr() 函数。下图示。
在这里更新以前A引用D的指针,让其引用C。能够看到,B由UNIQUE变为了MULTIPLE。伪代码以下。
copy_ptr(dest_ptr, src_ptr){ delete_ptr(dest_ptr) *dest_ptr = *src_ptr set_multiple_tag(dest_ptr) if(tag(src_ptr) == UNIQUE) set_multiple_tag(src_ptr) }
数 dest_ptr 和 src_ptr 分别表示的是目的指针和被复制的原指针。在上图图 中,A 的指针就是目的指针,B 的指针就是被复制的原指针。
最后把mutator的update_ptr()函数的调用全换成copy_ptr()就能实现1位引用计数法。
delete_ptr(ptr){ if(tag(ptr) == UNIQUE) reclaim(*ptr) }
只有当指针ptr的标签是UNIQUE时,它才会回收根据这个指针所引用的对象。由于当标签是MULTIPLE时,还可能存在其余引用这个对象的指针,因此它没法回收对象。
优势
不容易出现高速缓存缺失。缓存做为存储空间,比内存读取的快得多。若是须要的数据在缓存中,计算机就能进行高速处理。但若是缺失了的话就须要从内存中去读。这样一来浪费许多时间。
也就是说,当某个对象A要引用在内存中离他很远的B时,以往的引用计数法会在增减计数器的值时候去读B,从而致使高速缓存缺失,浪费时间。
1位引用计数法,它不须要在更新计数器的时候读取要引用的对象。指针直接复制就行,不必读取对象。由于不必给计数器留出多余的空间,因此节省了内存消耗量。这也不失为1位引用计数法的一个优势。
缺点 没办法处理计数器益处的对象。虽说,计数器的值通常都不足为2,可是若是少许固然能够放置无论。但咱们不能保证某一个任务不会出现不少计数器溢出的现象。