PHP内核探索之变量(1)Zval(自己看过不错儿)
作為數(shù)據(jù)的容器,我們常常需要跟變量打交道,不管這個(gè)變量是數(shù)字、數(shù)組、字符串、對象還是其他,因而可以說變量是構(gòu)成語言的不可或缺的基礎(chǔ)。本文是PHP內(nèi)核探索之變量的第一篇,主要介紹zval的基本知識,包括如下幾個(gè)方面的內(nèi)容:
由于寫作倉促,難免會(huì)有錯(cuò)誤,歡迎指出。
一、Zval的基本結(jié)構(gòu)
Zval是PHP中最重要的數(shù)據(jù)結(jié)構(gòu)之一(另一個(gè)比較重要的數(shù)據(jù)結(jié)構(gòu)是hash table),它包含了PHP中的變量值和類型的相關(guān)信息。它是一個(gè)struct,基本結(jié)構(gòu)為:
struct _zval_struct {zvalue_value value; /* value */zend_uint refcount__gc; /* variable ref count */zend_uchar type; /* active type */zend_uchar is_ref__gc; /* if it is a ref variable */ }; typedef struct _zval_struct zval;其中:
1. zval_value value
變量的實(shí)際值,具體來說是一個(gè)zvalue_value的聯(lián)合體(union):
typedef union _zvalue_value {long lval; /* long value */double dval; /* double value */struct { /* string */char *val;int len;} str;HashTable *ht; /* hash table value,used for array */zend_object_value obj; /* object */ } zvalue_value;2. zend_uint refcount__gc??
該值實(shí)際上是一個(gè)計(jì)數(shù)器,用來保存有多少變量(或者符號,symbols,所有的符號都存在符號表(symble table)中, 不同的作用域使用不同的符號表,關(guān)于這一點(diǎn),我們之后會(huì)論述)指向該zval。在變量生成時(shí),其refcount=1,典型的賦值操作如$a = $b會(huì)令zval的refcount加1,而unset操作會(huì)相應(yīng)的減1。在PHP5.3之前,使用引用計(jì)數(shù)的機(jī)制來實(shí)現(xiàn)GC,如果一個(gè)zval的refcount較少到0,那么Zend引擎會(huì)認(rèn)為沒有任何變量指向該zval,因此會(huì)釋放該zval所占的內(nèi)存空間。但,事情有時(shí)并不會(huì)那么簡單。后面我們會(huì)看到,單純的引用計(jì)數(shù)機(jī)制無法GC掉循環(huán)引用的zval,即使指向該zval的變量已經(jīng)被unset,從而導(dǎo)致了內(nèi)存泄露(Memory Leak)。
3. zend_uchar type
該字段用于表明變量的實(shí)際類型。在開始學(xué)習(xí)PHP的時(shí)候,我們已經(jīng)知道,PHP中的變量包括四種標(biāo)量類型(bool,int,float,string),兩種復(fù)合類型(array, object)和兩種特殊的類型(resource 和NULL)。在zend內(nèi)部,這些類型對應(yīng)于下面的宏(代碼位置 phpsrc/Zend/zend.h):
#define IS_NULL 0 #define IS_LONG 1 #define IS_DOUBLE 2 #define IS_BOOL 3 #define IS_ARRAY 4 #define IS_OBJECT 5 #define IS_STRING 6 #define IS_RESOURCE 7 #define IS_CONSTANT 8 #define IS_CONSTANT_ARRAY 9 #define IS_CALLABLE 104. is_ref__gc
這個(gè)字段用于標(biāo)記變量是否是引用變量。對于普通的變量,該值為0,而對于引用型的變量,該值為1。這個(gè)變量會(huì)影響zval的共享、分離等。關(guān)于這點(diǎn),我們之后會(huì)有論述。
正如名字所示,ref_count__gc和is_ref__gc是PHP的GC機(jī)制所需的很重要的兩個(gè)字段,這兩個(gè)字段的值,可以通過xdebug等調(diào)試工具查看。
二、xdebug的安裝配置
xdebug是一個(gè)開源的PHP 性能分析和debug工具。雖然對于一般的程序調(diào)試,var_dump,echo,print,debug_backtrace等常見的調(diào)試工具已經(jīng)基本夠用,但對于一些復(fù)雜的調(diào)試和性能測試,xdebug絕對是一個(gè)很好的幫手(其他的如Xhprof等工具也很優(yōu)秀)。
本文的基本環(huán)境:
安裝xdebug的基本過程為(實(shí)際上是源碼編譯一個(gè)擴(kuò)展):
1. 下載源碼包.
下載地址為:http://www.xdebug.org/docs/install
本文中下載的版本為:xdebug-2.6.tar.gz
2. 解壓
tar xvzf xdebug-2.6.tar.gz3. 在xdebug的目錄執(zhí)行phpize
4. ./configure?? 配置
5. Make&&? make install
這會(huì)生成xdebug.so擴(kuò)展文件(zend_extension),位置在xdebug/modules
6. 在php.ini中加載xdebug擴(kuò)展
zend_extension=your-xdebug-path/xdebug.so7. 添加xdebug的配置
xdebug.profiler_enable = on xdebug.default_enable = on xdebug.trace_output_dir="/tmp/xdebug" xdebug.trace_output_name = trace.%c.%p xdebug.profiler_output_dir="/tmp/xdebug" xdebug.profiler_output_name="cachegrind.out.%s"這里不再詳細(xì)介紹各個(gè)配置項(xiàng)的含義,詳細(xì)的請看:http://www.xdebug.org/docs/all?
現(xiàn)在,PHP中,應(yīng)該已經(jīng)有了Xdebug的擴(kuò)展信息(php –m,也可以phpinfo()):
?
現(xiàn)在,你的腳本中,可以通過xdebug_debug_zval打印Zval的信息:
| 1 2 3 4 | <?php ????$a = array( 'test' ); ????$a[] = &$a; ????xdebug_debug_zval( 'a' ); |
3. Zval的更多原理
(注,本部分主要參考:http://derickrethans.nl/collecting-garbage-phps-take-on-variables.html, 作者Derick Rethans是一位優(yōu)秀的PHP內(nèi)核專家,在全世界做過多次報(bào)告,都有相關(guān)的pdf下載,這里(http://derickrethans.nl/talks.html )有作者每次演講的記錄,很多都值得我們深入去學(xué)習(xí)研究)
前面我們已經(jīng)說過,PHP使用Zval這種結(jié)構(gòu)來保存變量,這里我們將繼續(xù)追蹤zval的更多細(xì)節(jié)。
1.?????? 創(chuàng)建變量時(shí),會(huì)創(chuàng)建一個(gè)zval.
| 1 2 | $str = "test zval"; xdebug_debug_zval('str'); |
輸出結(jié)果:
str: (refcount=1, is_ref=0)='test zval'當(dāng)使用$str="test zval";來創(chuàng)建變量時(shí),會(huì)在當(dāng)前作用域的符號表中插入新的符號(str),由于該變量是一個(gè)普通的變量,因此會(huì)生成一個(gè)refcount=1且is_ref=0的zval容器。也就是說,實(shí)際上是這樣的:
2.?????? 變量賦值給另外一個(gè)變量時(shí),會(huì)增加zval的refcount值。
| 1 2 3 4 | $str? = "test zval"; $str2 = $str; xdebug_debug_zval('str'); xdebug_debug_zval('str2'); |
輸出結(jié)果: ?????
同時(shí)我們看到,str和是str2這兩個(gè)symbol的zval結(jié)構(gòu)是一樣的。這里其實(shí)是PHP所做的一個(gè)優(yōu)化,由于str和str2都是普通變量,因而它們指向了同一個(gè)zval,而沒有為str2開辟單獨(dú)的zval。這么做,可以在一定程度上節(jié)省內(nèi)存。這時(shí)的str,str2與zval的對應(yīng)關(guān)系是這樣的:
?
3.?????? 使用unset時(shí),對減少相應(yīng)zval的refcount值
| 1 2 3 4 5 6 | $str? = "test zval"; $str3 = $str2 = $str; xdebug_debug_zval('str'); unset($str2,$str3) xdebug_debug_zval('str'); ?? |
結(jié)果為:
str: (refcount=3, is_ref=0)='test zval' str: (refcount=1, is_ref=0)='test zval'由于unset($str2,$str3)會(huì)將str2和str3從符號表中刪除,因此,在unset之后,只有str指向該zval,如下圖所示:
?
現(xiàn)在如果執(zhí)行unset($str),則由于zval的refcount會(huì)減少到0,該zval會(huì)從內(nèi)存中清理。這當(dāng)然是最理想的情況。
但是事情并不總是那么樂觀。
4.?????? 數(shù)組變量與普通變量生成的zval非常類似,但也有很大不同
與標(biāo)量這些普通變量不同,數(shù)組和對象這類復(fù)合型的變量在生成zval時(shí),會(huì)為每個(gè)item項(xiàng)生成一個(gè)zval容器。例如:
| 1 2 3 4 | $ar = array( ????'id'?? => 38, ????'name' => 'shine' ); xdebug_debug_zval('ar'); |
打印出zval的結(jié)構(gòu)是:
ar: (refcount=1, is_ref=0)=array ('id' => (refcount=1, is_ref=0)=38, 'name' => (refcount=1, is_ref=0)='shine' )如下圖所示:
?
可以看出,變量$ar生成的過程中,共生成了3個(gè)zval容器(紅色部分標(biāo)注)。對于每個(gè)zval而言,refcount的增減規(guī)則與普通變量的相同。例如,我們在數(shù)組中添加另外一個(gè)元素,并把$ar['name']的值賦給它:
| 1 2 3 4 5 6 7 | $ar = array( ????'id'?? => 38, ????'name' => 'shine' ); $ar['test'] = $ar['name']; xdebug_debug_zval('ar'); |
則打印出的zval為:
ar: (refcount=1, is_ref=0)=array ('id' => (refcount=1, is_ref=0)=38,'name' => (refcount=2, is_ref=0)='shine','test' => (refcount=2, is_ref=0)='shine' )如同普通變量一樣,這時(shí)候,name和test這兩個(gè)symbol指向同一個(gè)zval:
?
同樣的,從數(shù)組中移除元素時(shí),會(huì)從符號表中刪除相應(yīng)的符號,同時(shí)減少對應(yīng)zval的refcount值。同樣,如果zval的refcount值減少到0,那么就會(huì)從內(nèi)存中刪除該zval:
| 1 2 3 4 5 6 7 8 | $ar = array( ????'id'?? => 38, ????'name' => 'shine' ); $ar['test'] = $ar['name']; unset($ar['test'],$ar['name']); xdebug_debug_zval('ar'); |
輸出結(jié)果為:
ar: (refcount=1, is_ref=0)=array ('id' => (refcount=1, is_ref=0)=38)5.?????? 引用的出現(xiàn),會(huì)令zval的規(guī)則變得復(fù)雜
在加入引用之后,情況會(huì)變的稍微復(fù)雜一點(diǎn)。例如,在數(shù)組中添加對本身的引用:
| 1 2 3 | $a = $array('one'); $a[] = &$a; xdebug_debug_zval('a'); |
輸出的結(jié)果:
a: (refcount=2, is_ref=1)=array (0 => (refcount=1, is_ref=0)='one', 1 => (refcount=2, is_ref=1)=... )上述輸出中,…表示指向原始數(shù)組,因而這是一個(gè)循環(huán)的引用。如下圖所示:
?
現(xiàn)在,我們對$a執(zhí)行unset操作,這會(huì)在symbol table中刪除相應(yīng)的symbol,同時(shí),zval的refcount減1(之前為2),也就是說,現(xiàn)在的zval應(yīng)該是這樣的結(jié)構(gòu):
(refcount=1, is_ref=1)=array (0 => (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=... )也就是下圖所示的結(jié)構(gòu):
?
這時(shí),不幸的事情發(fā)生了!
Unset之后,雖然沒有變量指向該zval,但是該zval卻不能被GC(指PHP5.3之前的單純引用計(jì)數(shù)機(jī)制的GC)清理掉,因?yàn)閦val的refcount均大于0。這樣,這些zval實(shí)際上會(huì)一直存在內(nèi)存中,直到請求結(jié)束(參考SAPI的生命周期)。在此之前,這些zval占據(jù)的內(nèi)存不能被使用,便白白浪費(fèi)了,換句話說,無法釋放的內(nèi)存導(dǎo)致了內(nèi)存泄露。
如果這種內(nèi)存泄露僅僅發(fā)生了一次或者少數(shù)幾次,倒也還好,但如果是成千上萬次的內(nèi)存泄露,便是很大的問題了。尤其在長時(shí)間運(yùn)行的腳本中(例如守護(hù)程序,一直在后臺(tái)執(zhí)行不會(huì)中斷),由于無法回收內(nèi)存,最終會(huì)導(dǎo)致系統(tǒng)“再無內(nèi)存可用”。
6.?????? zval分離(Copy on write和change on write)
前面我們已經(jīng)介紹過,在變量賦值的過程中例如$b = $a,為了節(jié)省空間,并不會(huì)為$a和$b都開辟單獨(dú)的zval,而是使用共享zval的形式:
????????
那么問題來了:如果其中一個(gè)變量發(fā)生變化時(shí),如何處理zval的共享問題?
對于這樣的代碼:
| 1 2 3 4 5 6 7 8 9 10 11 | $a = "a simple test"; $b = $a; echo "before write:".PHP_EOL; xdebug_debug_zval('a'); xdebug_debug_zval('b'); $b = "thss"; echo "after write:".PHP_EOL; xdebug_debug_zval('a'); xdebug_debug_zval('b'); |
打印的結(jié)果是:
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指向了同一個(gè)zval(這么做的原因是節(jié)省內(nèi)存),而后$b發(fā)生了變化,Zend會(huì)檢查b指向的zval的refcount是否為1,如果是1,那么說明只有一個(gè)符號指向該zval,則直接更改zval。否則,說明這是一個(gè)共享的zval,需要將該zval分離出去,以保證單獨(dú)變化互不影響,這種機(jī)制叫做COW –Copy on write。在很多場景下,COW都是一種比較高效的策略。
那么對于引用變量呢?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | $a = 'test'; $b = &$a;<br> echo "before change:".PHP_EOL; xdebug_debug_zval('a'); xdebug_debug_zval('b');<br> $b = 12; echo "after change:".PHP_EOL; xdebug_debug_zval('a'); xdebug_debug_zval('b');<br> unset($b); echo "after unset:".PHP_EOL; xdebug_debug_zval('a'); xdebug_debug_zval('b'); |
輸出的結(jié)果為:
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)=12after unset: a: (refcount=1, is_ref=0)=12可以看出,在改變了$b的值之后,Zend會(huì)檢查zval的is_ref檢查是否是引用變量,如果是引用變量,則直接更改即可,否則,需要執(zhí)行剛剛提到的zval分離。由于$a 和 $b是引用變量,因而更改共享的zval實(shí)際上也間接更改了$a的值。而在unset($b)之后,變量$b從符號表中刪除了。
這里也說明一個(gè)問題,unset并不是清除zval,而只是從符號表中刪除相應(yīng)的symbol。這樣一來,之前很多的關(guān)于引用的疑問也可以理解了(下一節(jié)我們將深入探索PHP的引用)。
本文參考文獻(xiàn):
?
知識改變命運(yùn),碼農(nóng)拯救人生來源:http://www.cnblogs.com/ohmygirl/p/internal-variable-1.html
總結(jié)
以上是生活随笔為你收集整理的PHP内核探索之变量(1)Zval(自己看过不错儿)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 今年沙尘天气怎么这么多 主要原因其实在
- 下一篇: 股票后面sz什么意思