要想研究一個(gè)世界,要從宏觀和微觀兩個(gè)角度來(lái)看。研究php也是一樣的。直接寫(xiě)php代碼算是從宏觀角度去研究了php,而從微觀角度開(kāi)始研究php,自然就要從zval開(kāi)始了。
如果把php比作一個(gè)世界的話(huà),zval可以算是這個(gè)世界的原子了。它是php變量的實(shí)際存儲(chǔ)單元,貫通了整個(gè)php的世界。
zval是在Zend2中引入的,后來(lái)在php7中,zval的結(jié)構(gòu)做了擴(kuò)展。不過(guò)這次主要研究的是php5的zval,php7的有機(jī)會(huì)再研究。
首先拉下來(lái)一份php的源碼(https://github.com/php/php-src)。切到分支PHP-5.6.26,打開(kāi)Zend/zend_types.h,在55行能看到如下代碼:
而在Zend/zend.h的334行,能看到_zval_struct的定義。
可以看出,zval這個(gè)結(jié)構(gòu)體有4個(gè)部分,value保存了具體的值,而type則指明了這個(gè)具體值的類(lèi)型。refcount__gc和is_ref__gc從名字能看出跟gc有關(guān),但其實(shí)他們不僅僅是跟gc有關(guān),還有更重要的用途。
is_ref__gc說(shuō)明了這個(gè)zval是否被是引用的。而refcount__gc說(shuō)明了有幾個(gè)變量是這個(gè)值:比如有兩個(gè)變量都是一樣的值,那么在底層這兩個(gè)變量指向的都是同一個(gè)zval,然后refcount__gc會(huì)被設(shè)為2。如果有三個(gè)變量都指向這個(gè)值,那么refcount__gc就是3。如果其中一個(gè)變量發(fā)生變化的話(huà),才會(huì)建立新的zval,原來(lái)的zval的refcount__gc減1。這種機(jī)制就是寫(xiě)時(shí)復(fù)制,copy-on-write。
上面是zvalue_value的定義,這是個(gè)union。它可以被解讀為長(zhǎng)整數(shù),雙精度浮點(diǎn)數(shù),一個(gè)字符串指針和相應(yīng)的字符串長(zhǎng)度,hashtable指針,php對(duì)象或者是php常量的表達(dá)式抽象樹(shù)。具體采用哪種解讀,則是看zval結(jié)構(gòu)體的type字段。
接下來(lái)要隆重介紹一個(gè)壓箱底的函數(shù),它可以查看每個(gè)變量的zval:debug_zval_dump()。配合這個(gè)函數(shù),我們可以看看refcount__gc是怎么變化的。
這段代碼的結(jié)果如下:
我們來(lái)一個(gè)個(gè)分析。
首先是上面的$a、$b和$c,它們的refcount都是4,明明只有三個(gè)變量但為啥是4呢?其實(shí)除了它們3個(gè)自身之外,在函數(shù)調(diào)用傳值時(shí)也復(fù)制了一次變量,所以在3個(gè)之外還得加1,也就成了4。
而中間的三個(gè)怎么又變成了3、3、2呢?簡(jiǎn)單想想就能發(fā)現(xiàn),因?yàn)榘l(fā)生了寫(xiě)時(shí)復(fù)制,建立了新的zval給了$c,所以它的refcount成了1,傳入函數(shù)變成了2。而$a和$b指向的zval少了$c,自然refcount就會(huì)減1了。
至于最后三個(gè),$d成了$a的引用,需要改寫(xiě)zval的is_ref為1,于是發(fā)生了寫(xiě)時(shí)復(fù)制,所以$b的zval的refcount再次減1。而$a和$d都指向同一個(gè)zval,而且它們是引用就不再發(fā)生寫(xiě)時(shí)復(fù)制,自然refcount就保持在了1。
大概zval就介紹完了,其實(shí)很簡(jiǎn)單。但是有個(gè)思考題:
相信你這么聰明,應(yīng)該能想明白為啥改了$c[1],$a[1]也發(fā)生了變化了吧?