收集循环引用

一个实用的垃圾收集器大体上应该满足以下条件  
一、消除悬空指针和内存泄露  
二、不能给程序运行带来过高的额外开销,一般来说要控制在10%  
三、尽量减少停顿时间,使得运行平稳  
四、内存管理方面局部性尽量好  

其中第一条没什么好说的,肯定要符合,至于第四条,当然也很重要,局部性做好了可以成倍提高运行速度,不过,如果都是内存操作,就算没做好速度一般也可以接受了,在老式的系统中,由于会用磁盘等外部存储扩展内存,这条相对就重要些,现在可以看做是一个比较高级的要求  

于是,垃圾收集机制需要考虑的重点就在第二和第三条,可惜的是,这两条很多时候有冲突,比方说,引用计数机制保证了运行平稳,但付出代价就是运行时额外开销较高,而使用节点复制的时候,堆内存越大,收集频率就越小,平均耗在垃圾回收上的时间就少,但是可能由于可达集太大而长时间停顿,而如果增加垃圾收集频率,虽然更平稳了,但是开销又大了。很多垃圾收集器都在这两条中去了一个权衡,而具体的做法,就是将垃圾回收机制或思想混合使用  

引用计数拥有平稳运行的优点,对一般程序来说,其额外开销也能接受,但是由于有循环引用存在,使得很多垃圾收集器不能简单地使用引用计数,除非语言本身保证了不会循环引用。最简单直接的办法,就是用标记-清除来辅助回收循环引用  

当然标记-清除存在停顿运行的缺点,这两种机制如果结合不好,反而会造成短板效应,事实上人们已经花了很大的功夫研究如何对引用计数做一些修改,保持其优点的情况下解决循环引用问题,的确也有一些方案,可惜没有一个能用在实际中,这些方案的缺点在于:  
1 实现复杂,给程序带来的额外开销不可接受  
2 算法本身有缺陷,有时候会陷入死循环  
3 有的大部分情况下运行正常,但是某些情况下时间复杂度是指数级,会造成运行期更大的颠簸  
4 只能针对特定语言,不够普世  
因此,到目前为止,如果在实际中使用到引用计数,一般也都是用跟踪式垃圾收集作为辅助策略,比如python的标准实现中,垃圾收集机制就是引用计数为主,标记-清除为辅(也结合了分代收集机制,按下不表)  

和纯粹的标记-清除相比,对引用计数做辅助收集,只需要处理循环引用即可,因此没必要将所有申请的对象都记录下来,在标记阶段也只需要标记必要的对象,比如,python中的int,string等对象本身不是容器,就没必要进入这个流程,只需要记录所有容器即可,非容器的垃圾利用引用计数可以实时收集  

比如:  
class A: 
    pass 
a = A() #1号对象 
b = A() #2号对象 
a.a = 123 
a.b = b 
b.a = a 
b.b = "hello" 
在对象建立的时候,只会记录a和b两个instance对象,123和"hello"这两个对象就不记录了。在垃圾回收阶段,从根集只标记a和b两个对象。如果上面代码后面加上:  
del a, b  
则在回收的时候,最后得到的垃圾集合是两个instance  

对这个垃圾集合做回收的时候,需要注意算法只标记了容器类的,如果容器中有int或string等对象,还是需要通过引用计数来收集,因此,释放对象的时候,每个对象还是需要对它所引用的对象做引用计数操作  

python采用了引用拆除的办法,具体说就是找到一个垃圾集合后,任意取一个垃圾容器,将其清空(即所有引用归为NULL),清空的时候会减少其引用的所有对象的引用计数,这样一来就可能会释放掉一些对象,如果垃圾集合还不为空,就再选择一个  
比如上面的例子,首先选择1号对象,清空,这样一来123和2号对象的引用计数都减一,由于2号对象此时计数为0,则自动销毁,销毁的时候会将1号对象和"hello"的引用计数减一,1号对象的引用计数也为0了,销毁,由于1号对象已清空,所以就不用处理它的引用了,这样所有垃圾都被正常回收  
当然,这个清空可能并不是一次性的,如果1号对象先清理b属性,则产生连锁反应,最后一样能正常回收  

另一个办法是,对于所有检测到的垃圾先放入一个hash表中,然后循环销毁所有垃圾,销毁过程中会导致每个容器被清空,当某个元素引用计数减少为0时,判断它是不是已经在hash表,如果在,则不用销毁,否则就是int或string这种,正常销毁  

由于python是动态类型语言,因此采用了从容器和非容器进行区分的办法,无法对容器做进一步分类,实际上在这种情况下,追踪式垃圾收集只需要找循环引用即可,由于python的动态性,所有容器都有可能造成循环引用。如果能对容器做进一步区分,则可以缩小需要收集的对象区间,但这一般需要语言本身是静态性的,由编译器进行辅助,比如(java代码):  
class A 
{ 
    int a; 
    String b; 
} 
class B 
{ 
    A a; 
    float[] b; 
} 
class C 
{ 
    double a; 
    D b; 
} 
class D 
{ 
    C c; 
} 

对这三个类的数据结构做静态分析,可以发现,A中的元素,int不是容器,String虽然是个对象,但String内部是一个char[],不存在循环引用,因此A不会在循环引用中,同理,B也不会,而C的元素有D类型,D中的元素有C类型,因此C和D可能构成循环引用,这样一来,如果编译器给出足够的信息,虚拟机在运行时只需要记录C和D的对象建立,并对其做辅助收集就行了,其他对象均可通过引用计数正常回收
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值