ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 引用计数和写时拷贝 ## 参考 [引用计数基本知识](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机制做一个总结