# 引用计数和写时拷贝
## 参考
[引用计数基本知识](https://www.php.net/manual/zh/features.gc.refcounting-basics.php)
[PHP的垃圾回收机制](https://www.cnblogs.com/xuxubaobao/p/10840176.html)
[# PHP内核探索之变量---变量的容器-Zval](https://blog.csdn.net/ohmygirl/article/details/41542445)
## 1.引用计数(标量类型)
在上一章我们已经讲解了PHP变量的结构,其中了解到ref\_count\_\_gc和is\_ref\_\_gc是PHP的GC机制所需的很重要的两个字段,我们就写几个例子来看一下这两个变量是如何变化的。
**1.1创建变量时,会创建一个zval**
~~~
$str = "test zval";
xdebug_debug_zval('str');
结果:str: (refcount=1, is_ref=0)='test zval'
~~~
当使用$str="test zval";来创建变量时,会在当前作用域的符号表中插入新的符号(str),由于该变量是一个普通的变量,因此会生成一个refcount=1且is\_ref=0的zval容器。也就是说,实际上是这样的:
![](https://img.kancloud.cn/45/44/45443f0f62df0451c6d3a09e2cb182f1_363x144.png)
**1.2变量赋值给另外一个变量时,会增加zval的refcount值.**
~~~
$str = "test zval";
$str2 = $str;
xdebug\_debug\_zval('str');
xdebug\_debug\_zval('str2');
结果:
str: (refcount=2, is_ref=0)='test zval'
str2: (refcount=2, is_ref=0)='test zval'
~~~
同时我们看到,str和是str2这两个symbol的zval结构是一样的。这里其实是PHP所做的一个优化,由于str和str2都是普通变量,因而它们指向了同一个zval,而没有为str2开辟单独的zval。这么做,可以在一定程度上节省内存。这时的str,str2与zval的对应关系是这样的:
![](https://img.kancloud.cn/52/22/5222f9bcc493c6c1335d5cee90b44db0_401x174.png)
**1.3使用unset时,对减少相应zval的refcount值**
~~~
$str = "test zval";
$str3 = $str2 = $str;
xdebug\_debug\_zval('str');
unset($str2,$str3)
xdebug\_debug\_zval('str');
结果:
str: (refcount=3, is_ref=0)='test zval'
str: (refcount=1, is_ref=0)='test zval'
~~~
由于unset($str2,$str3)会将str2和str3从符号表中删除,因此,在unset之后,只有str指向该zval,如下图所示:
![](https://img.kancloud.cn/bc/0d/bc0d01b1db011b4d85827a074ae0c770_975x278.png)
现在如果执行unset($str),则由于zval的refcount会减少到0,该zval会从内存中清理。这当然是最理想的情况。
但是事情并不总是那么乐观。
## 2.引用计数(复合类型)
当考虑像array和object这样的复合类型时,事情就稍微有点复杂. 与标量(scalar)类型的值不同,array和object类型的变量把它们的成员或属性存在自己的符号表中。这意味着下面的例子将生成三个zval变量容器。
**2.1创建数组类型**
与标量这些普通变量不同,数组和对象这类复合型的变量在生成zval时,会为每个item项生成一个zval容器。例如:
~~~
$ar=array(
'id'=> 38,
'name'=>'shine'
);
xdebug\_debug\_zval('str');
结果:
ar: (refcount=1, is_ref=0)=array (
'id' => (refcount=1, is_ref=0)=38,
'name' => (refcount=1, is_ref=0)='shine'
)
~~~
![](https://img.kancloud.cn/f0/b0/f0b042093232a9e19116f337eac4d7ca_752x405.png)
**2.2添加一个已经存在的元素到数组中**
可以看出,变量$arr生成的过程中,共生成了3个zval容器(红色部分标注)。对于每个zval而言,refcount的增减规则与普通变量的相同。例如,我们在数组中添加另外一个元素,并把$arr\['name'\]的值赋给它:
~~~
$ar=array(
'id'=> 38,
'name'=>'shine',
);
$ar['test']=$ar[’name‘];
xdebug_debug_zval('str');
结果:
arr: (refcount=1, is_ref=0)=array (
'id' => (refcount=1, is_ref=0)=38,
'name' => (refcount=1, is_ref=0)='shine',
'test' => (refcount=1, is_ref=0)='shine',
)
~~~
如同普通变量一样,这时候,name和test这两个symbol指向同一个zval:
![](https://img.kancloud.cn/7b/a2/7ba2e0f256c67b572fabde9bfd4a5a40_736x316.png)
**2.2unset删除一个已经存在的元素到数组中**
同样的,从数组中移除元素时,会从符号表中删除相应的符号,同时减少对应zval的refcount值。同样,如果zval的refcount值减少到0,那么就会从内存中删除该zval:
~~~
$ar=array(
'id'=> 38,
'name'=>'shine',
);
$ar['test']=$ar[’name‘];
unset($ar['test'],$ar['name']);
xdebug_debug_zval('str');
结果:
ar: (refcount=1, is_ref=0)=array ('id' => (refcount=1, is_ref=0)=38)
~~~
![](https://img.kancloud.cn/3c/22/3c221841acd7ea78b41d20b947690e7f_644x235.png)
现在,当我们添加一个数组本身作为这个数组的元素时,事情就变得有趣,下个例子将说明这个。例中我们加入了引用操作符,否则php将生成一个复制。
**2.3把数组作为一个元素添加到自己**
~~~
$a \= array( 'one' );
$a\[\] =& $a;
xdebug\_debug\_zval( 'a' );
结果:
a: (refcount=2, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)
~~~
能看到数组变量 (a) 同时也是这个数组的第二个元素(1) 指向的变量容器中“refcount”为`2`。上面的输出结果中的"..."说明发生了递归操作, 显然在这种情况下意味着"..."指向原始数组。
![](https://img.kancloud.cn/1e/1e/1e1e7fca9f9809e2a60279a894070f87_646x219.png)
**2.4unset $a**
现在,我们对$a执行unset操作,这会在symbol table中删除相应的symbol,同时,zval的refcount减1(之前为2),也就是说,现在的zval应该是这样的结构:
~~~
(refcount=1, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=1, is_ref=1)=...
)
~~~
![](https://img.kancloud.cn/73/2f/732fab3ff5134d836f598b06102a2f8a_513x207.png)
这时,不幸的事情发生了!
Unset之后,虽然没有变量指向该zval,但是该zval却不能被GC(指PHP5.3之前的单纯引用计数机制的GC)清理掉,因为zval的refcount均大于0。这样,这些zval实际上会一直存在内存中,直到请求结束(参考SAPI的生命周期)。在此之前,这些zval占据的内存不能被使用,便白白浪费了,换句话说,无法释放的内存导致了内存泄露。
如果这种内存泄露仅仅发生了一次或者少数几次,倒也还好,但如果是成千上万次的内存泄露,便是很大的问题了。尤其在长时间运行的脚本中(例如守护程序,一直在后台执行不会中断),由于无法回收内存,最终会导致系统“再无内存可用”。
## 2.写时copy
前面我们已经介绍过,在变量赋值的过程中例如$b = $a,为了节省空间,并不会为$a和$b都开辟单独的zval,而是使用共享zval的形式:
![](https://img.kancloud.cn/3c/63/3c6321965808cc3ef60674da2c17080a_445x177.png)
**
**2.1如果其中一个变量发生变化时,如何处理zval的共享问题?**
~~~
$a = "a simple test";
$b = $a;
echo "before write:".PHP_EOL;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
$b = "thss"cho "after write:".PHP_EOL;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
结果:
before write:
a: (refcount=2, is_ref=0)='a simple test'
b: (refcount=2, is_ref=0)='a simple test'
after write:
a: (refcount=1, is_ref=0)='a simple test'
b: (refcount=1, is_ref=0)='thss
~~~
起初,符号表中a和b指向了同一个zval(这么做的原因是节省内存),而后$b发生了变化,Zend会检查b指向的zval的refcount是否为1,如果是1,那么说明只有一个符号指向该zval,则直接更改zval。否则,说明这是一个共享的zval,需要将该zval分离出去,以保证单独变化互不影响,这种机制叫做**COW**–Copy on write。在很多场景下,COW都是一种比较高效的策略。
**2.1那么对于引用变量呢?**
~~~
$a = 'test';
$b = &$a;
echo "before change:".PHP\_EOL;
xdebug\_debug\_zval('a');
xdebug\_debug\_zval('b');
$b = 12;
echo "after change:".PHP\_EOL;
xdebug\_debug\_zval('a');
xdebug\_debug\_zval('b');
unset($b);
echo "after unset:".PHP\_EOL;
xdebug\_debug\_zval('a');
xdebug\_debug\_zval('b');
结果:
before change:
a: (refcount=2, is_ref=1)='test'
b: (refcount=2, is_ref=1)='test'
after change:
a: (refcount=2, is_ref=1)=12
b: (refcount=2, is_ref=1)=12
after unset:
a: (refcount=1, is_ref=0)=12
~~~
可以看出,在改变了$b的值之后,Zend会检查zval的is\_ref检查是否是引用变量,如果是引用变量,则直接更改即可,否则,需要执行刚刚提到的zval分离。由于$a 和 $b是引用变量,因而更改共享的zval实际上也间接更改了$a的值。而在unset($b)之后,变量$b从符号表中删除了。
这里也说明一个问题,unset并不是清除zval,而只是从符号表中删除相应的symbol。
这一章我们讲述了变量引用计数的相关原理,在下一章我们会对PHP的GC机制做一个总结