编程语言的垃圾收集算法有很多种,今天我们谈一谈常见的集中垃圾收集算法。
引用计数算法
引用计数算法很简单,它实际上是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。
代码示例
php的垃圾回收算法使用的就是引用计数算法。这里我们以php为例来了解一下该算法。
<?php
$p = "abc";
xdebug_debug_zval('p');
/**
p: (refcount=1, is_ref=0)='abc'
$q = "def";
$p = $q;
xdebug_debug_zval( 'q' );
/**
q: (refcount=2, is_ref=0)='def'
unset($q);
xdebug_debug_zval( 'p' );
/**
p: (refcount=1, is_ref=0)='def'
循环引用问题
$b = 'one';
$a = [$b];
$a[] = &$a;
xdebug_debug_zval( 'a' );
xdebug_debug_zval( 'b' );
/**
a: (refcount=2, is_ref=1)=array (0 => (refcount=2, is_ref=0)='one', 1 => (refcount=2, is_ref=1)=...)
b: (refcount=2, is_ref=0)='one'
unsetting $a之后,我们发现b的引用计数还有2个:
<?php
unset($a);
xdebug_debug_zval('b');
/**
b: (refcount=2, is_ref=0)='one'
复现php的内存泄露
通过不断创建循环引用的变量,我们可以让php来实现内存泄露,代码如下:
class A{
private $b;
function __construct(){
$this->b = new B($this);
}
function __destruct(){
//echo "A destruct\n";
}
}
class B{
private $a;
function __construct($a){
$this->a = $a;
}
function __destruct(){
//echo "B descturct\n";
}
}
for($i=0;;$i++){
$a = new A();
if($i00 == 0){
echo memory_get_usage()."\n";
}
}
但是在实际的执行中,我们发现内存使用总是在一个范围内循环。原来在php 5.3.0以后,已经通过同步周期回收算法解决了此问题。
要想复现内存泄露,可以在执行代码前调用关闭gc函数来复现:
gc_disable();
再执行此脚本,没过多长时间,就报Fatal error: Allowed memory size of 134217728 bytes exhausted的错误了。
同步周期回收算法
那回收算法是如何实现垃圾回收的呢?
首先,我们先还原一下当时的场景:
<?php
$a = 'one';
$b = [$a];
$b[] = &$b;
unset($b);
xdebug_debug_zval('a');
回收算法示意图
为避免不得不检查所有引用计数可能减少的垃圾周期,这个算法把所有可能根(possible roots 都是zval变量容器),放在根缓冲区(root buffer)中(用紫色来标记,称为疑似垃圾),这样可以同时确保每个可能的垃圾根(possible garbage root)在缓冲区中只出现一次。仅仅在根缓冲区满了时,才对缓冲区内部所有不同的变量容器执行垃圾回收操作。看上图的步骤 A。
在步骤 B 中,模拟移除每个紫色变量。当紫色变量被移除时,可能将不是紫色的普通变量引用数减"1",如果某个普通变量引用计数变成0了,就对这个普通变量再做一次模拟移除。每个变量只能被模拟移除一次,模拟移除后标记为灰。
在步骤 C 中,模拟恢复每个紫色变量。恢复是有条件的,当变量的引用计数大于0时才对其做模拟恢复。同样每个变量只能恢复一次,恢复后标记为黑,基本就是步骤 B 的逆运算。这样剩下的一堆没能恢复的就是该删除的蓝色节点了,在步骤 D 中遍历出来真的删除掉。
java为什么不用引用计算实现垃圾回收
引用知乎里的回答
实际上,在内存充裕的前提下,tracing GC的整体开销比引用计数方式更低一些,所以吞吐量(throughput)高一些。因为引用计数方式通常需要统计冗余的局部信息,而tracing GC则可以通过全局信息一口气批量判断对象的生死;如果是带整理的tracing GC,则其内存分配通常也会更快。不过tracing GC通常会比引用计数方式的延迟(latency)大一些,而且内存越紧张的时候tracing GC的效率反而越低,所以在内存不太充裕的地方使用引用计数仍然是个合理的选择(例如iOS5上的ARC)。
作者:RednaxelaFX
链接:https://www.zhihu.com/question/21539353/answer/18596488
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
参考资料:
引用计数算法
引用计数基本知识
PHP的内存泄露问题与垃圾回收
回收周期(Collecting Cycles)
垃圾回收机制中,引用计数法是如何维护所有对象引用的?
网友评论