深入理解 PHP7 中全新的 zval 容器和引用計數機制
最近在查閱 PHP7 垃圾回收的資料的時候,網上的一些程式碼示例在本地環境下執行時出現了不同的結果,使我一度非常迷惑。 仔細一想不難發現問題所在:這些文章大多是 PHP5.x 時代的,而 PHP7 釋出後,採用了新的 zval 結構,相關的資料也比較貧瘠,所以我結合一些資料做了一個總結, 主要側重於解釋新 zval 容器中的引用計數機制 ,如有謬誤,還望不吝指教。
PHP7 中新的 zval 結構
明人不說暗話,先看程式碼!
struct _zval_struct { union { zend_longlval;/* long value */ doubledval;/* double value */ zend_refcounted*counted; zend_string*str; zend_array*arr; zend_object*obj; zend_resource*res; zend_reference*ref; zend_ast_ref*ast; zval*zv; void*ptr; zend_class_entry *ce; zend_function*func; struct { uint32_t w1; uint32_t w2; } ww; } value; union { struct { ZEND_ENDIAN_LOHI_4( zend_uchartype,/* active type */ zend_uchartype_flags, zend_ucharconst_flags, zend_ucharreserved)/* call info for EX(This) */ } v; uint32_t type_info; } u1; union { uint32_tvar_flags; uint32_tnext;/* hash collision chain */ uint32_tcache_slot;/* literal cache slot */ uint32_tlineno;/* line number (for ast nodes) */ uint32_tnum_args;/* arguments number for EX(This) */ uint32_tfe_pos;/* foreach position */ uint32_tfe_iter_idx;/* foreach iterator index */ } u2; }; 複製程式碼
對於該結構的詳細描述可以參考文末鳥哥的文章,寫的非常詳細,我就不關公面前耍大刀了,這裡我只提出幾個比較關鍵的點:
- PHP7 中的變數分為 變數名 和 變數值 兩部分,分別對應
zval_struct
和在其中宣告的value
-
zval_struct.value
中的zend_long
、double
都是 簡單資料型別 ,能夠直接儲存具體的值,而其他複雜資料型別儲存一個指向其他資料結構的 指標 - PHP7 中,引用計數器儲存在
value
中而不是zval_struct
- NULL 、 布林型 都屬於 沒有值 的資料型別(其中布林型通過
IS_FALSE
和IS_TRUE
兩個常量來標記),自然也就沒有引用計數 - 引用 (REFERENCE)變為了一種資料結構而不再只是一個標記位了,它的結構如下:
struct _zend_reference { zend_refcounted_h gc; zvalval; } 複製程式碼
-
zend_reference
作為zval_struct
中包含的一種value
型別,也擁有自己的val
值,這個值是指向一個zval_struct.value
的。他們都擁有自己的 引用計數器 。
引用計數器用來記錄當前有多少 zval
指向同一個 zend_value
。
針對第六點,請看如下程式碼:
$a = 'foo'; $b = &$a; $c = $a; 複製程式碼
此時的資料結構是這樣的:
$a 與 $b 各擁有一個 zval_struct
容器,並且其中的 value
都指向同一個 zend_reference
結構, zend_reference
內嵌一個 val
結構, 指向同一個 zend_string
, 字串的內容 就儲存在其中。
而 $c 也擁有一個 zval_struct
,而它的 value 在初始化的時候可以直接指向上面提到的 zend_string
,這樣在拷貝時就不會產生複製。
下面我們就聊一聊在這種全新的 zval
結構中,會出現的種種現象,和這些現象背後的原因。
問題
一. 為什麼某些變數的引用計數器的初始值為 0
現象
$var_int = 233; $var_float = 233.3; $var_str = '233'; xdebug_debug_zval('var_int'); xdebug_debug_zval('var_float'); xdebug_debug_zval('var_str'); /** 輸出 ** var_int: (refcount=0, is_ref=0)int 233 var_float: (refcount=0, is_ref=0)float 233.3 var_str: (refcount=0, is_ref=0)string '233' (length=3) **********/ 複製程式碼
原因
在 PHP7 中,為一個變數賦值的時候,包含了兩部分操作:
- 為符號量(即變數名)申請一個
zval_struct
結構 - 將變數的值儲存到
zval_struct.value
中 對於zval
在value
欄位中能儲存下的值,就不會在對他們進行引用計數, 而是在拷貝的時候直接賦值 ,這部分型別有:
- IS_LONG
- IS_DOUBLE
即我們在 PHP 中的 整形 與 浮點型 。
那麼 var_str 的 refcount 為什麼也是 0 呢?
這就牽扯到 PHP 中字串的兩種型別:
-
interned string
內部字串(函式名、類名、變數名、靜態字串):
$str = '233';// 靜態字串 複製程式碼
- 普通字串:
$str = '233' . time(); 複製程式碼
對於 內部字串 而言,字串的內容是唯一不變的,相當於 C 語言中定義在靜態變數區的字串, 他們的生存週期存在於整個請求期間,request 完成後會統一銷燬釋放 ,自然也就無需通過引用計數進行記憶體管理。
二. 為什麼在對整形、浮點型和靜態字串型變數進行引用賦值時,計數器的值會直接變為2
現象
$var_int_1 = 233; $var_int_2 = &var_int; xdebug_debug_zval('var_int_1'); /** 輸出 ** var_int: (refcount=2, is_ref=1)int 233 **********/ 複製程式碼
原因
回憶一下我們開頭講的 zval_struct
中 value
的資料結構,當為一個變數賦 整形 、 浮點型 或 靜態字串 型別的值時,value 的資料型別為 zend_long
、 double
或 zend_string
,這時值是可以直接儲存在 value 中的。而按值拷貝時,會開闢一個新的 zval_struct
以同樣的方式將值儲存到相同資料型別的 value 中,所以 refcount 的值一直都會為 0。
但是當使用 &
操作符進行引用拷貝時,情況就不一樣了:
- PHP 為
&
操作符操作的變數申請一個zend_reference
結構 - 將
zend_reference.value
指向原來的zval_struct.value
-
zval_struct.value
的資料型別會被修改為zend_refrence
- 將
zval_struct.value
指向剛剛申請並初始化後的zend_reference
- 為新變數申請
zval_struct
結構,將他的value
指向剛剛建立的zend_reference
此時:var_int_2 都擁有一個 zval_struct
結構體,並且他們的 zval_struct.value
都指向了同一個 zend_reference
結構,所以該結構的引用計數器的值為 2。
題外話:zend_reference 又指向了一個整形或浮點型的 value,如果指向的 value 型別是 zend_string,那麼該 value 引用計數器的值為 1。而 xdebug 出來的 refcount 顯示的是 zend_reference 的計數器值(即 2)
三. 為什麼初始陣列的引用計數器的值為 2
現象
$var_empty_arr = [1, 2, '3']; xdebug_debug_zval('var_empty_arr'); /** 輸出 ** var_arr: (refcount=3, is_ref=0) array (size=3) 0 => (refcount=0, is_ref=0)int 1 1 => (refcount=0, is_ref=0)int 2 2 => (refcount=1, is_ref=0)string '3' (length=1) **********/ 複製程式碼
原因
這牽扯到 PHP7 中的另一個概念,叫做 immutable array
(不可變陣列)。 關於 immutable array
的詳細介紹我放到下篇文章中講,這裡我們只需要知道,這樣定義的陣列,叫做 不可變陣列 。
For arrays the not-refcounted variant is called an "immutable array". If you use opcache, then constant array literals in your code will be converted into immutable arrays. Once again, these live in shared memory and as such must not use refcounting. Immutable arrays have a dummy refcount of 2, as it allows us to optimize certain separation paths.
不可變陣列和我們上面講到的 內部字串 一樣,都是 不使用引用計數 的,但是不同點是,內部字串的計數值恆為 0,而不可變陣列會使用一個 偽計數值 2。