要想研究一个世界,要从宏观和微观两个角度来看。研究php也是一样的。直接写php代码算是从宏观角度去研究了php,而从微观角度开始研究php,自然就要从zval开始了。
如果把php比作一个世界的话,zval可以算是这个世界的原子了。它是php变量的实际存储单元,贯通了整个php的世界。
zval是在Zend2中引入的,后来在php7中,zval的结构做了扩展。不过这次主要研究的是php5的zval,php7的有机会再研究。
首先拉下来一份php的源码(https://github.com/php/php-src)。切到分支PHP-5.6.26,打开Zend/zend_types.h,在55行能看到如下代码:
而在Zend/zend.h的334行,能看到_zval_struct的定义。
可以看出,zval这个结构体有4个部分,value保存了具体的值,而type则指明了这个具体值的类型。refcount__gc和is_ref__gc从名字能看出跟gc有关,但其实他们不仅仅是跟gc有关,还有更重要的用途。
is_ref__gc说明了这个zval是否被是引用的。而refcount__gc说明了有几个变量是这个值:比如有两个变量都是一样的值,那么在底层这两个变量指向的都是同一个zval,然后refcount__gc会被设为2。如果有三个变量都指向这个值,那么refcount__gc就是3。如果其中一个变量发生变化的话,才会建立新的zval,原来的zval的refcount__gc减1。这种机制就是写时复制,copy-on-write。
上面是zvalue_value的定义,这是个union。它可以被解读为长整数,双精度浮点数,一个字符串指针和相应的字符串长度,hashtable指针,php对象或者是php常量的表达式抽象树。具体采用哪种解读,则是看zval结构体的type字段。
接下来要隆重介绍一个压箱底的函数,它可以查看每个变量的zval:debug_zval_dump()。配合这个函数,我们可以看看refcount__gc是怎么变化的。
这段代码的结果如下:
我们来一个个分析。
首先是上面的$a、$b和$c,它们的refcount都是4,明明只有三个变量但为啥是4呢?其实除了它们3个自身之外,在函数调用传值时也复制了一次变量,所以在3个之外还得加1,也就成了4。
而中间的三个怎么又变成了3、3、2呢?简单想想就能发现,因为发生了写时复制,建立了新的zval给了$c,所以它的refcount成了1,传入函数变成了2。而$a和$b指向的zval少了$c,自然refcount就会减1了。
至于最后三个,$d成了$a的引用,需要改写zval的is_ref为1,于是发生了写时复制,所以$b的zval的refcount再次减1。而$a和$d都指向同一个zval,而且它们是引用就不再发生写时复制,自然refcount就保持在了1。
大概zval就介绍完了,其实很简单。但是有个思考题:
相信你这么聪明,应该能想明白为啥改了$c[1],$a[1]也发生了变化了吧?