在上一篇文章中,介绍了在GC机制中,GC是以什么标准断定对象能够被标记的,以及最有效最经常使用的可达性分析法。
今天介绍另一种很是经常使用的标记算法,它的应用面也至关普遍。这就是:
引用计数法 Reference Counting
这个算法的本质,其实就是上篇文章中判断一个对象要被回收的另一种思路,即若是没有其它对象调用当前对象,那么当前对象就能够被回收了。判断有多少调用当前对象有两种方法,一种是看看其它对象,有多少对象持有当前对象的引用。还有一种办法就是,当前对象自身实现一个计数机制。统计来自外界引用的调用。第一个办法就是上篇文章中可达性分析的最初思路。而第二个办法就是如今要介绍的引用计数法的最初思路:咱们不关心谁保存了咱们的引用,咱们只关心保存咱们引用的对象究竟有多少个。
在引用计数法中,每一个对象拥有一个记录自身引用被持有的个数,当这个对象的计数器的值为0时,也就是再也不有其它对象持有该对象的引用了,那么也就是再也不有对象能够调用到当前对象的方法或者变量了。这一刻也就是当前对象能够被回收的时刻了。
在《垃圾回收的算法与实现》这本书中,对于该算法又一个颇有意思的描述;
每一个对象就像是一个明星。这个对象的引用计数的大小,就像是这个明星的人气指数。当明星的人气指数为0时,也就是这个明星黯然离场的时候了。
在引用计数法中,每一个对象的引用计数器初始(防盗链接:本文首发自http://www.cnblogs.com/jilodream/ )值为0。没当有一个新对象持有当前对象的引用时,计数器就会加1。没当有一个已经持有引用的对象消失,或者抛弃持有的引用时。计数器就会-1。当计数器的值再次为0,这个计数器所表明的对象就会被回收掉。(更准确的说,是会让空闲链表持有本身的引用,将本身所占用的内存空间标记为能够从新分配的区域)。
接下来讲说这个算法的优缺点:
优势:
一、随时随地的回收垃圾
当计数器变为0的瞬间,当前对象就会被放置到空闲队列中,做为能够被从新分配的内存空间。而其余的GC算法,如以前讲到的可达性分析法,都须要进行一次全局的清理,才会统一的清理掉这个周期内的全部已知的垃圾空间。
二、最大暂停时间短
引用计数法师在每次生成、或销毁对象或者是变动指针的时候进行一次计算的,所以对程序的影响时间是很是短暂的。
而其余的GC算法则因为须要统一的清除或者是复制等,因此暂停的时间会比较长,对程序的影响也比较长。有时这个时间长到性能上已经没法忍受时,就须要不断的调优,减短单次暂停的最大时长。
三、核心思路简单
引用计数法。不须要从根节点依次开始进行遍历。每一个对象只关心直接持有本身引用的对象是否发生了变化。这样当对象发生回收时,也只影响这个对象直接持有的引用对象,而不会直接影响到更深路径的对象。
如A持有B/C,B持有D/E。当A被回收时,只会影响B,C对象的引用计数器。当B的计数器值所以降为0时,才会影响到D/E节点。总体的计算成本很是的低。而其余的GC方式须要从根依次进行遍历。整个过程很是复杂(如涉及到一个网状的引用关系,如何终止掉无心义的遍历就尤其重要)。
缺点:
一、计算的频率过快
每次执行一条命令时,均可能会引发若干次的引用计数变化。尤为是对于一些根节点持有的对象(从根对象)的引用计数,其变化的速度更是惊人。所以计数器的工做量很是繁重。
二、计数器所占用的内存空间很是的大
引用计数法中。每一个对象都须要一个属于本身的引用计数器,尽管这个计数器使用无符号型来存储,可是也只能节约一个bit位的空间。因为可能会发生全部对象都持有一个对象的极端条件,因此计数器所容许的最大值必定是要很是大。(防盗链接:本文首发自http://www.cnblogs.com/jilodream/ )相应的所占用的控件也会很是的大。这是一种是典型的空间换取时间的算法。
三、实现很是复杂,一旦出错后果会很是的严重
尽管该算法的优势是思路很是简单,可是实现起来却要复杂的多。每当一个对象回收时,都须要刷新每一处使用到的对象的计数器。一旦有一处错误,则可能会出现永远没法修复的内存泄露问题。
四、循环依赖
这个问题能够是引用计数法被公认的最难处理问题:当两个(也可使是多个)内存对象互相依赖,同时也不与外界有引用关系,从而造成一种相似孤岛链的关系。此时每个对象计数器都不为0,GC也就没法回收掉这些内存了。这种状况下,典型的引用计数法是没法解决掉的。每每须要结合其余的回收算法,进行改良才能解决问题。算法
针对这些缺点。业界提供了不少的改良算法
一、延迟引用计数法 Deffered Reference Counting
deferred [dɪ'fɜ:d] adj. 延期的,缓召的;
针对从根对象的引用很是频繁的更新,从而致使其计数器的计算任务很是繁重的这个问题。有人提出了一种特别的思路:不维护从根引用对象的计数器。这些计数器的值始终为0。其余对象仍然正常采用引用计数器的方式。可是这就会有一个问题,GC没法判断哪些对象是能够回收的,哪些是不能回收。所以就须要把计数器降为0(decr_ref_cnt函数)的对象暂时先放置在一个容器中,延迟它的回收。这个容器称为ZCT(Zero Count Table)。它专门用来记录那些计数器通过减持计算而变为0的对象。函数
以下图:性能
那么何时开始真正的标记垃圾对象呢?
通常来讲当咱们建立(new_obj函数)对象时,发现已经没有空余的内存空间能够分配时。就会进行一次ZCT扫描(scan_zct函数)来清理掉这些对象。而后再次尝试分配内存,若是仍然不能成功,那么这时候就认为内存溢出了。
ZCT扫描(scan_zct函数)的步骤以下:
(1)首先将从根引用的计数器调整到正常的数值;
可见下图:指针
(2)而后遍历ZCT,将值为0的对象都清理掉(delete(obj)函数),放置到前文说的空闲队列中。(此时这些对象的空间就能够被用来分配新的对象了)
(3)而后将全部的从根引用的计数器再调整回去。对象
这个算法的好处是,废弃掉从根引用对象的计数器被频繁刷新这些无心义的繁重耗时的操做,大大减轻了处理器的负担。
固然它的缺点也很明显,内存再也不会被当即回收掉。只有当内存空间不够时(防盗链接:本文首发自http://www.cnblogs.com/jilodream/ ),才开始扫描ZCT,统一进行回收。而扫描ZCT的耗时通常会随着ZCT的增大而增大,这样就致使了GC的最大的暂停时间变大。固然也能够经过调小ZCT来减少最大暂停时间,可是这样又会让GC更频繁的进行ZCT扫描(空间与时间不可兼得)。从而致使内存回收处理的吞吐量降低。二、Sticky引用计数法
sticky 英 [ˈstɪki] adj. 粘性的; 不动的;
前文有提到计数器的控件占用很是的大。这是为了保证极端场景计数下,计数器能够正常使用。但这种算法却偏偏是将计数器所占用的空间(计数上限)缩小。这是由于对于大部分对象来讲,计数器中所能达到的最大值都不大,对象很快就会被回收了。为了保证计数器每个对象的计数器都不会溢出,而给每个对象都开辟一块很是大的空间来计数,这是一种很是愚蠢的行为。
对于个别计数器会溢出的对象来讲:
(1)那么就让它溢出好了
反正它都被这么多对象引用了,几率上讲,基本也不太会被回收了,能够说默认它为永生对象了。
(2)若是认为计数器溢出很差,能够加入从根寻址的变相算法,大体思路是这个样子的:
<1>计数器归0
<2>从根依次寻址,增长引用计数值
<3>清理掉引用计数器为0的对象
这个算法的好处是:
<1>下降计数器占用的空间
<2>清理掉循环引用的场景
三、1位引用计数算法
这个算法能够说是Sticky引用计数法的一种极端体现,也就是计数器只有1位。一旦出现共有一个内存的场景下就“溢出”了。
尽管场景很极端,可是他表明了不少的内存:这些对象从创造出来以后,只被一个对象持有,不存在多个对象共同持有的状况。
这种算法中,计数器更多像是一个标记值,标记当前对象是被其余对象引用,而再也不是一个对象的计数器。
四、部分标记清除算法
这个算法能够说是纯粹就是为了解决循环依赖的。算法的大体思路以下:将内存对象标记为四种状态:
A绝对不是垃圾的对象;
B绝对是垃圾的对象;
C可能存在循环引用的对象;
D搜索完毕的对象。
这样就能够针对对象的状态采用不一样的回收算法计算。因为内存中的大部分对象都处于循环引用的孤岛连以外(防盗链接:本文首发自http://www.cnblogs.com/jilodream/ )。所以大部分的对象仍然采用的是引用计数法进行计算。只有少部分的对象可能存在循环引用中,所以只对这部分对象进行进行根可达性的计算。
尽管引用计数法自身存在诸多的缺陷,可是仍然有不少地方采用了这种回收算法:如Python的GC、Flash player的内存管理等。惋惜因为循环依赖等缘由,到目前为止,主流的JVM还均没有将引用计数法做为GC的回收算法来使用。blog
对于这篇文章中提到的这些GC标记算法,以及这些GC标记算法的实现,在这里推荐一本前文提到的书:《垃圾回收的算法与实现》。这本书写的很是详细,书中的插图也很是形象。有兴趣的同窗能够找来阅读下。队列