Reference Counting GC (Part one)
引用计数法
George E.Collins.1960.
引用计数算法中引入了一个概念计数器。计数器代表对象被引用的次数,它是无符号正整数用于计数器的位数根据算法和实现有所不同。本文结构如下图所示。
在变更数组元素等的时候会进行指针的更新。通过更新指针,可能会产生没有被任何程序引用的垃圾对象。引用计数法中会监督在更新指针的时候是否有产生垃圾,从而在产生 垃圾时将其立刻回收。这样,可以说将内存管理和 mutator 同时运行正是引用计数法的一大特征。
计数器值的增减
引用计数算法中,mutator没有明确启动GC的语句。它在mutator的处理过程中通过增减计数器的值来进行内存管理,这两种情况下计数器的值会发生改变,这使用到了new_obj()函数和update_ptr()函数。
new_obj()和update_ptr()函数
new_obj()生成对象
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次。
update_ptr()更新指针ptr,对计数器进行增减
update_ptr(ptr, obj){
inc_ref_cnt(obj) //计数器+
dec_ref_cnt(*ptr) //计数器-
*ptr = obj // 重新指向 obj
}
在mutator更新指针时会执行此程序。其、*ptr = obj是指针更新的部分。
增加引用和删除引用
inc_ref_cnt(obj)对指针ptr新引用对象obj计数器进行增量操作。
inc_ref_cnt(obj){
obj.ref_cnt++
}
仅仅是对对象计数器进行自增操作。
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)
}
- 首先对更新指针之前引用的对象*ptr的计数器进行减量操作。减量操作后,计数器的值为0的对象变成了“垃圾”。因此,这个对象的指针会全部被删除。
- 然后递归调用dec_ref_cnt(*ptr)对孩子进行计数器减量的操作。
- 然后通过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。
update_ptr()函数执行情况
初始状态下从根引用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()函数
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()函数
也要修改一线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()函数
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-- // 用完了在给根加回去。
}
- 程序把所有通过根直接引用的对象的计数器都进行增量。这样才算把根引用反映到了计数器的值上。
- 调查所有与$zct相连的对象,如果存在计数器值为0的对象,则将此对象从$zct中删除(delete)。
// 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()函数的频率就增加了,也压低了吞吐量。很明显这样就本末倒置了。
Stricky引用计数法
什么是Stricky引用计数法
在引用计数法中,我们有必要花功夫来研究一件事,那就是要为计数器设置多大的位宽。 假设为了反映所有引用,计数器需要1个字(32位机器就是32位)的空间。但是这样会大量消耗内存空间。打个比方,2 个字的对象就要附加1个字的计数器。也就是说,计数器害得对象所占空间增大了1.5 倍。
对此我们有个方法,那就是用来减少计数器位宽的“Sticky引用计数法”。举个例子我们假设用于计数器的位数为5位,那么这种计数器最多只能数到2的5次方减1也就是31个引用数。如果此对象被大于31个对象引用,那么计数器就会溢出。这跟车辆速度计的指针爆表是一个状况。
针对计数器溢出(也就是爆表的对象),需要暂停对计数器的管理。对付这种对象,我们主要有两种方法。
什么都不做
对于计数器溢出的对象,我们可以这样处理:不再增减计数器的值,就把它放着,什么也不做。不过这样一来,即使这个对象成了垃圾(即被引用数为 0),也不能将其回收。也就是说,白白浪费了内存空间。然而事实上有很多研究表明,很多对象一生成马上就死了。也就是说, 在很多情况下,计数器的值会在0到1的范围内变化,鲜少出现 5 位计数器溢出这样的情况。 此外,因为计数器溢出的对象在执行中的程序里占有非常重要的地位,所以可想而知,其将 来成为垃圾的可能性也很低。也就是说,不增减计数器的值,就把它那么放着也不会有什么大问题。 考虑到以上事项,对于计数器溢出的对象,什么也不做也不失为一个可用的方法。
使用GC标记-清除算法进行管理
另一个方法是,可以使用标记清除算法来辅助。但是需要对标记清除进行修改。
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点不同之处。
- 一开始把所有对象的计数器置为0.
- 不标记对象,而是对计数器进行增量操作。
- 为了对计数器进行增量操作,算法对活动对象进行了不止一次的搜索。
像这样,只要把引用计数法和 GC 标记 - 清除算法结合起来,在计数器溢出后即使对象 成了垃圾,程序还是能回收它。另外还有一个优点,那就是还能回收循环的垃圾。
但是在进行标记处理之前,必须重置所有的对象和计数器。此外,因为在查找对象时没 有设置标志位而是把计数器进行增量,所以需要多次(次数和被引用数一致)查找活动对象。 考虑到这一点的话,显然在这里进行的标记处理比以往的 GC 标记 - 清除算法中的标记处理 要花更多的时间。也就是说,吞吐量会相应缩小。
1位引用计数法
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 指针”。
copy_ptr()函数
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 的指针就是被复制的原指针。
- 首先调用delete_ptr()函数,尝试回收dest_ptr引用对象。
- 把src_ptr复制到dest_ptr。
- 把指针src_ptr以及dest_ptr的标签更新为MULTIPLE。
- tag()函数返回实参的标签,返回UNIQUE或者MULTIPLE的任意一值。
- set_multiple_tag()函数则把指针变成MULTIPLE指针。
最后把mutator的update_ptr()函数的调用全换成copy_ptr()就能实现1位引用计数法。
delete_ptr()函数
delete_ptr(ptr){
if(tag(ptr) == UNIQUE)
reclaim(*ptr)
}
只有当指针ptr的标签是UNIQUE时,它才会回收根据这个指针所引用的对象。因为当标签是MULTIPLE时,还可能存在其他引用这个对象的指针,所以它无法回收对象。
优缺点
优点
不容易出现高速缓存缺失。缓存作为存储空间,比内存读取的快得多。如果需要的数据在缓存中,计算机就能进行高速处理。但如果缺失了的话就需要从内存中去读。这样一来浪费许多时间。
也就是说,当某个对象A要引用在内存中离他很远的B时,以往的引用计数法会在增减计数器的值时候去读B,从而导致高速缓存缺失,浪费时间。
1位引用计数法,它不需要在更新计数器的时候读取要引用的对象。指针直接复制就行,没必要读取对象。因为没必要给计数器留出多余的空间,所以节省了内存消耗量。这也不失为1位引用计数法的一个优点。
缺点
没办法处理计数器益处的对象。虽然说,计数器的值一般都不足为2,但是如果少量当然可以放置不管。但我们不能保证某一个任务不会出现很多计数器溢出的现象。
Reference Counting GC (Part one)的更多相关文章
- Reference Counting GC (Part two :Partial Mark & Sweep)
目录 部分标记清除算法 前提 dec_ref_cnt()函数 new_obj()函数 scan_hatch_queue()函数 paint_gray()函数 scan_gray()函数 collect ...
- 2. 引用计数法(Reference Counting)
1960年,George E. Collins 在论文中发布了引用计数的GC算法. 引用计数法意如了一个概念,那就是"计数器",计数器表示的是对象的人气指数, 也就是有多少程序引用 ...
- JSONKit does not support Objective-C Automatic Reference Counting(ARC) / ARC forbids Objective-C objects in struct
当我们在使用JSONKit处理数据时,直接将文件拉进项目往往会报这两个错“JSONKit does not support Objective-C Automatic Reference Coun ...
- iOS开发 JSonKit does not support Objective-C Automatic Reference Counting(ARC)
有使用JSonKit的朋友,如果遇到“JSonKit does not support Objective-C Automatic Reference Counting(ARC)”这种情况,可参照如下 ...
- [转]关于NSAutoreleasePool' is unavailable: not available in automatic reference counting mode的解决方法
转载地址:http://blog.csdn.net/xbl1986/article/details/7216668 Xcode是Version 4.2 Build 4D151a 根据Objective ...
- 错误解决:release' is unavailable: not available in automatic reference counting mode
解决办法: You need to turn off Automatic Reference Counting. You do this by clicking on your project in ...
- not available in automatic reference counting mode
UncaughtExceptionHandler.m:156:47: 'autorelease' is unavailable: not available in automatic referenc ...
- error: 'release' is unavailable: not available in automatic reference counting,该怎么解决
编译出现错误: 'release' is unavailable: not available in automatic reference counting mode.. 解决办法: You nee ...
- xcode禁用ARC(Automatic Reference Counting)
Automatic Reference Counting,自动引用计数,即ARC,可以说是WWDC2011和iOS5所引入的最大的变革和最激动人心的变化.ARC是新的LLVM 3.0编译器的一项特性, ...
随机推荐
- 转载:爱加密邀您参加5月17日深圳App开发沙龙活动
听闻,移动互联网的九大门派已集结在一起,各路顶尖高手携App修炼宝典九阴真经现身,一场席卷全国的App修炼风暴即将来临.5月17日,以“移动开发者如何白手起家.快速盈利”为主题的沙龙活动将在深圳市南山 ...
- HDU 4585 Shaolin(STL map)
Shaolin Time Limit:1000MS Memory Limit:32768KB 64bit IO Format:%I64d & %I64u Submit cid= ...
- JavaWeb利用cookie记住账号
JavaWeb利用cookie记住账号. 首先,来看看界面什么样子. 记住账号最普遍的做法,就是在点击登录时,将账号保存到cookie中. 材料准备 <script src="${ct ...
- 1、初始JAVA
一. 语言的翻译分两种: 编译型语言:例如c语言.c++语言 优点:速度快 缺点:不能跨平台 编译器编译——>特定平台的目标文件obj——>特定平台 ...
- 46. AngularJS所有版本下载
转自:https://www.cnblogs.com/best/tag/Angular/ 官网下载:https://angularjs.org/ AngularJS所有版本下载:https://cod ...
- 熟悉了下HTTP协议
HTML是一种用来定义网页的文本,会HTML,就可以编写网页: HTTP是在网络上传输HTML的协议,用于浏览器和服务器的通信.200表示一个成功的响应,后面的OK是说明.失败的响应有404 Not ...
- 9.优先队列,priority_queue
#include <iostream> #include <queue> #include <deque> #include <list> using ...
- 使用U盘安装win8(win8.1)系统
下载镜像 下载系统镜像,可以选32位或64位.下完后放到非系统盘(如果需要重新分区的,要先保存到移动硬盘或U盘) 使用U盘制作PE 准备一个4G以上的U盘(备份好U盘里的资料,因为刻录PE需要格式化U ...
- Map<String,String>转换json字符串
import java.util.HashMap; import java.util.Map; import net.sf.json.JSONObject; public class testJson ...
- 对比学习:《深度学习之Pytorch》《PyTorch深度学习实战》+代码
PyTorch是一个基于Python的深度学习平台,该平台简单易用上手快,从计算机视觉.自然语言处理再到强化学习,PyTorch的功能强大,支持PyTorch的工具包有用于自然语言处理的Allen N ...