PHP核心探索之變數(1)Zval(自己看過不錯兒)
作為資料的容器,我們常常需要跟變數打交道,不管這個變數是數字、陣列、字串、物件還是其他,因而可以說變數是構成語言的不可或缺的基礎。本文是PHP核心探索之變數的第一篇,主要介紹zval的基本知識,包括如下幾個方面的內容:
- Zval的基本結構
- 檢視zval的方法:debug_zval_dump和xdebug
- Zval的原理,COW等
由於寫作倉促,難免會有錯誤,歡迎指出。
一、Zval的基本結構
Zval是PHP中最重要的資料結構之一(另一個比較重要的資料結構是hash table),它包含了PHP中的變數值和型別的相關資訊。它是一個struct,基本結構為:
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
變數的實際值,具體來說是一個zvalue_value的聯合體(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
該值實際上是一個計數器,用來儲存有多少變數(或者符號,symbols,所有的符號都存在符號表(symble table)中, 不同的作用域使用不同的符號表,關於這一點,我們之後會論述)指向該zval。在變數生成時,其refcount=1,典型的賦值操作如$a = $b會令zval的refcount加1,而unset操作會相應的減1。在PHP5.3之前,使用引用計數的機制來實現GC,如果一個zval的refcount較少到0,那麼Zend引擎會認為沒有任何變數指向該zval,因此會釋放該zval所佔的記憶體空間。但,事情有時並不會那麼簡單。後面我們會看到,單純的引用計數機制無法GC掉迴圈引用的zval,即使指向該zval的變數已經被unset,從而導致了記憶體洩露(Memory Leak)。
3. zend_uchar type
該欄位用於表明變數的實際型別。在開始學習PHP的時候,我們已經知道,PHP中的變數包括四種標量型別(bool,int,float,string),兩種複合型別(array, object)和兩種特殊的型別(resource 和NULL)。在zend內部,這些型別對應於下面的巨集(程式碼位置 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 10
4. is_ref__gc
這個欄位用於標記變數是否是引用變數。對於普通的變數,該值為0,而對於引用型的變數,該值為1。這個變數會影響zval的共享、分離等。關於這點,我們之後會有論述。
正如名字所示,ref_count__gc和is_ref__gc是PHP的GC機制所需的很重要的兩個欄位,這兩個欄位的值,可以通過xdebug等除錯工具檢視。
二、xdebug的安裝配置
xdebug是一個開源的PHP 效能分析和debug工具。雖然對於一般的程式除錯,var_dump,echo,print,debug_backtrace等常見的除錯工具已經基本夠用,但對於一些複雜的除錯和效能測試,xdebug絕對是一個很好的幫手(其他的如Xhprof等工具也很優秀)。
本文的基本環境:
安裝xdebug的基本過程為(實際上是原始碼編譯一個擴充套件):
1. 下載原始碼包.
下載地址為:http://www.xdebug.org/docs/install
本文中下載的版本為:xdebug-2.6.tar.gz
2. 解壓
tar xvzf xdebug-2.6.tar.gz
3. 在xdebug的目錄執行phpize
4. ./configure 配置
5. Make&& make install
這會生成xdebug.so擴充套件檔案(zend_extension),位置在xdebug/modules
6. 在php.ini中載入xdebug擴充套件
zend_extension=your-xdebug-path/xdebug.so
7. 新增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"
這裡不再詳細介紹各個配置項的含義,詳細的請看:http://www.xdebug.org/docs/all
現在,PHP中,應該已經有了Xdebug的擴充套件資訊(php –m,也可以phpinfo()):
現在,你的指令碼中,可以通過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是一位優秀的PHP核心專家,在全世界做過多次報告,都有相關的pdf下載,這裡(http://derickrethans.nl/talks.html )有作者每次演講的記錄,很多都值得我們深入去學習研究)
前面我們已經說過,PHP使用Zval這種結構來儲存變數,這裡我們將繼續追蹤zval的更多細節。
1. 建立變數時,會建立一個zval.
1
2
|
$str
= "test zval" ; xdebug_debug_zval( 'str' ); |
輸出結果:
str: (refcount=1, is_ref=0)='test zval'
當使用$str="test zval";來建立變數時,會在當前作用域的符號表中插入新的符號(str),由於該變數是一個普通的變數,因此會生成一個refcount=1且is_ref=0的zval容器。也就是說,實際上是這樣的:
2. 變數賦值給另外一個變數時,會增加zval的refcount值。
1
2
3
4
|
$str
= "test zval" ; $str2
= $str ; xdebug_debug_zval( 'str' ); xdebug_debug_zval( 'str2' ); |
輸出結果:
str: (refcount=2, is_ref=0)='test zval'
str2: (refcount=2, is_ref=0)='test zval'
同時我們看到,str和是str2這兩個symbol的zval結構是一樣的。這裡其實是PHP所做的一個優化,由於str和str2都是普通變數,因而它們指向了同一個zval,而沒有為str2開闢單獨的zval。這麼做,可以在一定程度上節省記憶體。這時的str,str2與zval的對應關係是這樣的:
3. 使用unset時,對減少相應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' ); |
結果為:
str: (refcount=3, is_ref=0)='test zval'
str: (refcount=1, is_ref=0)='test zval'
由於unset($str2,$str3)會將str2和str3從符號表中刪除,因此,在unset之後,只有str指向該zval,如下圖所示:
現在如果執行unset($str),則由於zval的refcount會減少到0,該zval會從記憶體中清理。這當然是最理想的情況。
但是事情並不總是那麼樂觀。
4. 陣列變數與普通變數生成的zval非常類似,但也有很大不同
與標量這些普通變數不同,陣列和物件這類複合型的變數在生成zval時,會為每個item項生成一個zval容器。例如:
1
2
3
4
|
$ar
= array ( 'id'
=> 38, 'name'
=> 'shine' ); xdebug_debug_zval( 'ar' ); |
列印出zval的結構是:
ar: (refcount=1, is_ref=0)=array ( 'id' => (refcount=1, is_ref=0)=38, 'name' => (refcount=1, is_ref=0)='shine' )
如下圖所示:
可以看出,變數$ar生成的過程中,共生成了3個zval容器(紅色部分標註)。對於每個zval而言,refcount的增減規則與普通變數的相同。例如,我們在陣列中新增另外一個元素,並把$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' )
如同普通變數一樣,這時候,name和test這兩個symbol指向同一個zval:
同樣的,從陣列中移除元素時,會從符號表中刪除相應的符號,同時減少對應zval的refcount值。同樣,如果zval的refcount值減少到0,那麼就會從記憶體中刪除該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' ); |
輸出結果為:
ar: (refcount=1, is_ref=0)=array ('id' => (refcount=1, is_ref=0)=38)
5. 引用的出現,會令zval的規則變得複雜
在加入引用之後,情況會變的稍微複雜一點。例如,在陣列中新增對本身的引用:
1
2
3
|
$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=1)=... )
上述輸出中,…表示指向原始陣列,因而這是一個迴圈的引用。如下圖所示:
現在,我們對$a執行unset操作,這會在symbol table中刪除相應的symbol,同時,zval的refcount減1(之前為2),也就是說,現在的zval應該是這樣的結構:
(refcount=1, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=... )
也就是下圖所示的結構:
這時,不幸的事情發生了!
Unset之後,雖然沒有變數指向該zval,但是該zval卻不能被GC(指PHP5.3之前的單純引用計數機制的GC)清理掉,因為zval的refcount均大於0。這樣,這些zval實際上會一直存在記憶體中,直到請求結束(參考SAPI的生命週期)。在此之前,這些zval佔據的記憶體不能被使用,便白白浪費了,換句話說,無法釋放的記憶體導致了記憶體洩露。
如果這種記憶體洩露僅僅發生了一次或者少數幾次,倒也還好,但如果是成千上萬次的記憶體洩露,便是很大的問題了。尤其在長時間執行的指令碼中(例如守護程式,一直在後臺執行不會中斷),由於無法回收記憶體,最終會導致系統“再無記憶體可用”。
6. zval分離(Copy on write和change on write)
前面我們已經介紹過,在變數賦值的過程中例如$b = $a,為了節省空間,並不會為$a和$b都開闢單獨的zval,而是使用共享zval的形式:
那麼問題來了:如果其中一個變數發生變化時,如何處理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' ); |
列印的結果是:
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指向了同一個zval(這麼做的原因是節省記憶體),而後$b發生了變化,Zend會檢查b指向的zval的refcount是否為1,如果是1,那麼說明只有一個符號指向該zval,則直接更改zval。否則,說明這是一個共享的zval,需要將該zval分離出去,以保證單獨變化互不影響,這種機制叫做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' ); |
輸出的結果為:
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)=12 after unset: a: (refcount=1, is_ref=0)=12
可以看出,在改變了$b的值之後,Zend會檢查zval的is_ref檢查是否是引用變數,如果是引用變數,則直接更改即可,否則,需要執行剛剛提到的zval分離。由於$a 和 $b是引用變數,因而更改共享的zval實際上也間接更改了$a的值。而在unset($b)之後,變數$b從符號表中刪除了。
這裡也說明一個問題,unset並不是清除zval,而只是從符號表中刪除相應的symbol。這樣一來,之前很多的關於引用的疑問也可以理解了(下一節我們將深入探索PHP的引用)。
本文參考文獻:
- 鳥哥的深入變數引用/分離 http://www.laruence.com/2008/09/19/520.html
- 本文的主要參考文獻 http://derickrethans.nl/collecting-garbage-phps-take-on-variables.html
- http://blog.csdn.net/phpkernel/article/details/5732784
- http://www.jb51.net/article/50080.htm
- http://www.nowamagic.net/librarys/veda/detail/1442
來源:http://www.cnblogs.com/ohmygirl/p/internal-variable-1.html
相關文章
- php核心分析(五)-zvalPHP
- PHP底層核心原始碼之變數PHP原始碼變數
- PHP核心探索之PHP中的雜湊表PHP
- Golang之變數去哪兒?Golang變數
- PHP核心定義變數的方式PHP變數
- 聊聊Netty那些事兒之從核心角度看IO模型Netty模型
- PHP核心研究:HASH表和變數薦PHP變數
- [PHP核心探索]PHP中的雜湊表PHP
- php除錯——輸出變數值PHP除錯變數
- Erlang那些事兒第1回之我是變數,一次賦值永不改變變數賦值
- php底層原理之變數(二)PHP變數
- php底層原理之變數(一)PHP變數
- PHP變數PHP變數
- 迴圈不變數之荷蘭國旗變數
- PHP核心探索:寫時複製COW機制PHP
- 透過華為軍團看科技之變(六):智慧公路
- 探索ring0之核心概述
- PHP 核心特性 - 錯誤處理PHP
- 通過String的不變性案例分析Java變數的可變性Java變數
- PHP-Php-fpm:報錯找不到Java_Home等環境變數PHPJava變數
- MySQL核心技術之“pthead區域性變數”MySql變數
- 變數1變數
- PHP的可變變數名PHP變數
- 透過華為軍團看科技之變(五):智慧園區
- php 核心探祕之 PHP_FUNCTION 巨集PHPFunction
- PHP變數型別PHP變數型別
- PHP 常量與變數PHP變數
- 如何理解DDD中的不變性,不變數變數
- 【轉載】Linux核心除錯之使用模組引數Linux除錯
- 徹底搞懂 PHP 變數結構體,多數文章觀點不準確PHP變數結構體
- 透過華為軍團看科技之變(一):廣域網路
- php中將驗證變數,,防止非法變數PHP變數
- RPC 核心,萬變不離其宗RPC
- 不點兒之Linux檔案系統Linux
- PHP 核心分析(1):sapi_module_structPHPAPIStruct
- PHP基礎__變數、常量PHP變數
- php中的SERVER變數PHPServer變數
- 【深入 PHP】PHP7 的基本變數PHP變數