一个实用的垃圾收集器大体上应该满足以下条件
一、消除悬空指针和内存泄露
二、不能给程序运行带来过高的额外开销,一般来说要控制在10%
三、尽量减少停顿时间,使得运行平稳
四、内存管理方面局部性尽量好
其中第一条没什么好说的,肯定要符合,至于第四条,当然也很重要,局部性做好了可以成倍提高运行速度,不过,如果都是内存操作,就算没做好速度一般也可以接受了,在老式的系统中,由于会用磁盘等外部存储扩展内存,这条相对就重要些,现在可以看做是一个比较高级的要求
于是,垃圾收集机制需要考虑的重点就在第二和第三条,可惜的是,这两条很多时候有冲突,比方说,引用计数机制保证了运行平稳,但付出代价就是运行时额外开销较高,而使用节点复制的时候,堆内存越大,收集频率就越小,平均耗在垃圾回收上的时间就少,但是可能由于可达集太大而长时间停顿,而如果增加垃圾收集频率,虽然更平稳了,但是开销又大了。很多垃圾收集器都在这两条中去了一个权衡,而具体的做法,就是将垃圾回收机制或思想混合使用
引用计数拥有平稳运行的优点,对一般程序来说,其额外开销也能接受,但是由于有循环引用存在,使得很多垃圾收集器不能简单地使用引用计数,除非语言本身保证了不会循环引用。最简单直接的办法,就是用标记-清除来辅助回收循环引用
当然标记-清除存在停顿运行的缺点,这两种机制如果结合不好,反而会造成短板效应,事实上人们已经花了很大的功夫研究如何对引用计数做一些修改,保持其优点的情况下解决循环引用问题,的确也有一些方案,可惜没有一个能用在实际中,这些方案的缺点在于:
1 实现复杂,给程序带来的额外开销不可接受
2 算法本身有缺陷,有时候会陷入死循环
3 有的大部分情况下运行正常,但是某些情况下时间复杂度是指数级,会造成运行期更大的颠簸
4 只能针对特定语言,不够普世
因此,到目前为止,如果在实际中使用到引用计数,一般也都是用跟踪式垃圾收集作为辅助策略,比如python的标准实现中,垃圾收集机制就是引用计数为主,标记-清除为辅(也结合了分代收集机制,按下不表)
和纯粹的标记-清除相比,对引用计数做辅助收集,只需要处理循环引用即可,因此没必要将所有申请的对象都记录下来,在标记阶段也只需要标记必要的对象,比如,python中的int,string等对象本身不是容器,就没必要进入这个流程,只需要记录所有容器即可,非容器的垃圾利用引用计数可以实时收集
比如:
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代码):
对这三个类的数据结构做静态分析,可以发现,A中的元素,int不是容器,String虽然是个对象,但String内部是一个char[],不存在循环引用,因此A不会在循环引用中,同理,B也不会,而C的元素有D类型,D中的元素有C类型,因此C和D可能构成循环引用,这样一来,如果编译器给出足够的信息,虚拟机在运行时只需要记录C和D的对象建立,并对其做辅助收集就行了,其他对象均可通过引用计数正常回收
一、消除悬空指针和内存泄露
二、不能给程序运行带来过高的额外开销,一般来说要控制在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的对象建立,并对其做辅助收集就行了,其他对象均可通过引用计数正常回收