// PHP 变量的 写时复制(cow) 和 写时改变(cow) 笔记
// since: 2014-12-15
// liuzhengyi
// ref:
zval简介
PHP是一个弱类型的语言,所有的数据类型,在PHP内部都用zval这个数据结构表示.
zval的结构如下:
typedef struct _zval_struct {
zvalue_value value; // 存储真实的数据值, 是一个结构体
zvalue_uint ref_count; // 引用计数
zvalue_uchar type; // 变量类型
zvalue_uchar is_ref; // 是否是一个引用变量(&)
} zval;
debug_zval_dump() 方法可以dump出变量的一些zval信息.如:
$a = 'hi';
debug_zval_dump($a);
// string(2) "hi" refcount(2)
这里输出的refcount就是该变量的zval的引用计数.
PHP引擎为了节约内存使用,会尽量重用zval:
当执行 $b = $a; 时,
PHP并不会重新分配内存,生成一个新的zval. 而是会在符号表中增加一条记录,让$b和$a共用一个zval.
同时将这个zval的引用计数增加1.
当执行 $b=&$a; 时,$b和$a也是共用一个zval,引用计数也会增加.
但是$b和$a互为引用(使用同一个zval,并且值联动改变),为了记录这个信息,zval的is_ref会置为1.
注意, 当 $b = $a时, 两者的值不会联动改变.
而当 $b = &$a 时, 两者的值会联动改变.
PHP是通过判断 zval中的ref_count和is_ref的值来做到这一点的.
写时复制
考虑如下代码:
$a = 'hi';
$b = $a;
$b = 'hello';
第一行将生成一个zval,存储内容为'hi', ref_count=1, is_ref=0.
第二行不会生成新的zval, $b和$a共用一个zval, 同时zval的 ref_count自增1, is_ref不变(为0).
第三行的时候,会发生什么呢?
在对$b进行写(赋值)时,PHP检查其zval的ref_count和is_ref.
当发现其ref_count>1,is_ref=0时,就知道了$b和其他变量共用一个zval,并且其值*不应该*和共用者联动变化.
于是,PHP会新生成一个zval,存储$b的值"hello".
$b之前和$a共用的zval,现在不在和$b有任何关系了.
我们就说$b从之前的zval中分离出来了,之前的zval的ref_count会自减1.
这就是写时复制,当对共用zval的非引用变量进行赋值(写)时,PHP会复制一份zval出来.
写时改变
考虑如下代码:
$a = 'hi';
$b = &$a;
$b = 'hello';
第一行将生成一个zval,存储内容为'hi', ref_count=1, is_ref=0.
第二行不会生成新的zval, $b和$a共用一个zval, 同时zval的 ref_count自增1, is_ref也*置为1*.
第三行的时候,对$b重新赋值,$a的值也应该改变,这又是如何做到的呢?
同样, 在对$b进行写(赋值)时,PHP检查其zval的ref_count和is_ref.
当发现其ref_count>1,is_ref=1时,就知道了$b和其他变量共用一个zval,并且其值*应该*和共用者联动变化.
于是,PHP不会对$b进行分离,而是直接对应zval的值,于是共用这个zval的$a的内容也发生了变化.
此即为写时改变.
两种复合情况
考虑如下代码:
$a = 'hi';
$b = &$a;
$c = $b;
第一行将生成一个zval,存储内容为'hi', ref_count=1, is_ref=0.
第二行不会生成新的zval, $b和$a共用一个zval, 同时zval的 ref_count自增1, is_ref也*置为1*.
第三行的时候,将$b赋值给$c.此时会发生什么?
$c 获得 $b的值的拷贝,但不会和$a,$b建立引用关系.
所以无法和$b共用zval(如果共用zval,则无法表示$a和$b互为引用,而和$c不是引用关系).
于是,会新生成一个zval,供$c使用,ref_count=1, is_ref=0.
而$a 和 $b 还是共用之前的zval.
再考虑如下代码:
$a = 'hi';
$b = $a;
$c = &$b;
第一行将生成一个zval,存储内容为'hi', ref_count=1, is_ref=0.
第二行不会生成新的zval, $b和$a共用一个zval, 同时zval的 ref_count自增1, is_ref不变,为0.
第三行的时候,将$c设置为$b的引用.此时又会发生什么?
$b和$c互为引用,而和$a没有引用关系,一个zval无法表示,还是需要新生成一个zval.
本来$a和$b共用一个zval,现在$b和$c要互为引用,必须共用一个zval,所以需要将$b和$a分离开.
所以,这里先将$b和$a分离,生成一个新的zval,然后将$b和$c绑定到一个zval上,并将is_ref设为1.
ref_count和is_ref的另外一种情况
前面说的几种情况都能对应到:
ref_count>1 && is_ref=0
ref_count>1 && is_ref=1
剩下的情况呢,就是 ref_count<2.
ref_count<2,说明这个变量独立使用一个zval,修改时不需要考虑其他变量.
其实就是这种情况:
$a = 'hi';
$a = 'hello';
没有分离,也没有复制.
debug_zval_dump()的输出
前面提到:
$a = 'hi';
debug_zval_dump($a);
会输出 ref_count = 2.
但是当执行:
$b = &$a;
debug_zval_dump($b);
则会输出 ref_count = 1.
这是为什么呢?
PHP中简单变量作为函数参数传递时,是按值传递的,即会传递变量的一个复制.
所以将一个独占zval的变量传递给debug_zval_dump()时,传参时的复制操作使该zval的ref_count增加了1.
而将一个共用引用zval(is_ref=1)的变量传递给debug_zval_dump()时,传参时的复制操作导致变量分离.
最终导致传入的变量的zval的ref_count=1.
笔记一篇,欢迎批评讨论!