一个php变量存储在一个叫做“zval” 的地方,一个zval 结构包含了什么呢,包含了变量的类型和值,和两个附加信位元信息,第一个位叫做“is_ref”, 它是个布尔值,它标识了这个变量是不是个引用类型,通过这个位元,PHP引擎了解了这个变量是普通类型的变量还是引用类型的变量。因为php允许通过 &操作符让用户获得一个引用。一个zval容器则通过一个叫做引用技术的机制来优化内存的占用。附加的两个位第二个位叫做”refcount”,包含了有多少变量名(这里叫做symbols)指向了这 “一个”zval容器。 php的所有变量符号保存在一个叫做符号表的地方,并且保存每一个变量的周期和范围。范围包括完整的周期,或者每一个函数或方法内部。
当一个变量通过一个常量值建立的时候,一个zval 容器被建立。例如:
<?php |
在上例中, 一个新的符号名“a” 被建立在当前范围内(作用域),并且建立了一个类型为“string”,值为”new string” 的新的变量容器, 这时因为目前还没有个用户建立的引用指向它,所以 “is_ref”默认为false, “refcount” 被设置为1,表示只有一个符号被用于这个变量容器。注意,如果”refcount” 为1,则”is_ref”永远为”false”. 如果你使用 xdebug ,可以通过它开查看响应信息:
<?php |
将会显示:
a: (refcount=1, is_ref=0) = ‘new string’ |
下面,赋值给其他变量名,将增加引用计数
<?php |
将会显示:
a:(refcount =2,is_ref=0) = 'new string' |
这里 refcount 为 2,因为同一个变量容器连接到了符号“a” 和”b”, php 有足够的聪明判断是否在不需要一个实际的变量容器的时候而复制一个,当”refcount”变为0的时候,变量容器将被摧毁, 当连接到变量容器的变量符号离开作用域(比如,函数结束)或者在符号表上调用unset()的时候,”refcount” 将会被减少1, 下面的例子说明了这个:
<?php $a ='new string'; $c =$b = $a; xdebug_debug_zval('a'); unset($b,$c); xdebug_debug_zval('a'); ?> |
将会显示:
a :(refcount = 3,is_ref=0)=’new sring’ a: (refcount=1,is_ref=0) = ‘new string’ |
如果我们现在调用”unset(a);”则变量容器,包括内部的值和类型,将会从内存移出。
复合类型
在数组和对象 等复合类型上,事情变得有点复杂,和标量类型不同的是,数组和对象分别保存它们的属性在它们自己的一个符号表内。 下面的例子将会建立三个zval容器:
<?php $a= array('meaning'=>'life','number'=>42); xdebug_debug_zval('a'); ?> |
将会显示:
a: (refcount=1,is_ref=0) = array( 'meaning'= >(refcount=2,is_ref=0) = 'life', 'number'=>(refcount=1,is_ref=0)=42, ) |
或者用图表示:
这三个zval 容器分别为 “a”,”meaning” 和”number”,简单的规则同样适用于它们对”refcount” 的增减:
我们添加一个其他的元素到一个存在的数组:
<?php $a= array('meaning'=>'life','number'=>42); $a['life']= $a['meaning']; xdebug_debug_zval('a'); ?> |
将会显示:
a: (refcount=1,is_ref=0) = array( 'meaning'=>(refcount=2,is_ref=0)= 'life', 'number'=>(refcount=1,is_ref=0)= 42, 'life'=>(refcount=2,is-ref=0)='life' ) |
用图来显示:
通过上面的xdebug输出, 我们看到新的数组元素和旧的元素现在指向了一个zval容器,并且zval容器的”refcount” 为2,当然,它们表示了两个zval容器,但是它们是同一个,。xdebug_debug_zval(0 函数并不能很好的表现出这一点,但是通过内存指针的显示看出,从数组中移出一个元素行为类似离开符号的作用域,数组元素指向的变量容器的”refcount”将会减1,同样当”refcount” 到0的时候,这个变量容器将会从内存中删除。 下面例子表示了这个:
<?php $a =array('meaning'=>'life','number'=>42); $a['life']= $a['meaning']; unset($a['meaning'],$a['number']); xdebug_debug_zval('a'); ?> |
将会如下显示:
a: (refount=1,is_ref=0) = array( 'life'=>(refcount=1,is_ref=0)='life' ) |
接下来,事情变得越来越有趣了,下面的例子中, 我们将要添加数组本身给数组的一个元素,我们在这里讲使用一个引用操作,要不php将会使用一个copy 操作来执行:
<?php $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)= ........ ) |
用图来显示:
就想你所看到的那样, 数组变量(“a”) 和它的第二个元素(“1”) 现在指向了一个函数容器,这个函数容器的”refcount”为2, 下面的”...” 显示出这里是个递归的调用,当然,这里的情况是这个。。。 表示它指向了原数组。
就像之前介绍的那样,unset 一个变量将会删除一个符号,并且zval函数容器的引用计数将会被减1,所以在上面的代码之后,如果我们unset 变量$a,, $a 和元素 “1” 指向的变量容器引用计数将会减1,从2变为1
将会表示如下:
(refcount=1,is_ref=1) = array( 0=>(refcount=1,is_ref=0)='one', 1=>(refcount=1,is_ref=1)=.... ) |
图表示为:
解决这个问题
尽管现在已经没有一个任意作用域的符号指向这个结构,但是它的内存还是不能释放,因为数组元素”1” 一直指向同一数组, 因为已经没有任何外部符号指向它,所以用户没有任何方法来清除这个结构(memoryleak!!)这样你将得到一个内存泄露。 幸运的是,在页面请求结束时,PHP将会清除这个数据结构。但是之前,它讲一直在内存中占用空间,这种情况会在你实现了一个分析算法或者其他的子元素包含“父级“元素的时候常常发生,当然同样的情况也会在对象上发生那个,实际上大部分会在对象上发生。因为一个对象尝尝隐含的通过引用来使用。
当然,如果这个问题仅仅发生一次或者两次也不算什么, 但是如果发生上千次,上万次,将会造成打量的内存损失,尤其是在长期运行的脚本中,例如守护进程,或者很大型的单元测试环境中。