PHP核心探索之變數(1)Zval

來源:互聯網
上載者:User
作為資料的容器,我們常常需要跟變數打交道,不管這個變數是數字、數組、字串、對象還是其他,因而可以說變數是構成語言的不可或缺的基礎。本文是PHP核心探索之變數的第一篇,主要介紹zval的基本知識,包括如下幾個方面的內容:

  1. Zval的基本結構
  2. 查看zval的方法:debug_zval_dump和xdebug
  3. 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 = onxdebug.default_enable = onxdebug.trace_output_dir="/tmp/xdebug"xdebug.trace_output_name = trace.%c.%pxdebug.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的資訊:

$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.

$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值。

$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值

$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']的值賦給它:

$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:

$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的規則變得複雜

在加入引用之後,情況會變的稍微複雜一點。例如,在數組中添加對本身的引用:

$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的共用問題?

對於這樣的代碼:

$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都是一種比較高效的策略。

那麼對於引用變數呢?

$a = 'test';$b = &$a;
echo "before change:".PHP_EOL;xdebug_debug_zval('a');xdebug_debug_zval('b');
$b = 12;echo "after change:".PHP_EOL;xdebug_debug_zval('a');xdebug_debug_zval('b');
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)=12b: (refcount=2, is_ref=1)=12after 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的引用)。

本文參考文獻:

  1. 鳥哥的深入變數引用/分離 http://www.laruence.com/2008/09/19/520.html
  2. 本文的主要參考文獻 http://derickrethans.nl/collecting-garbage-phps-take-on-variables.html
  3. http://blog.csdn.net/phpkernel/article/details/5732784
  4. http://www.jb51.net/article/50080.htm
  5. http://www.nowamagic.net/librarys/veda/detail/1442
  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.