详谈PHP垃圾回收机制
生活随笔
收集整理的這篇文章主要介紹了
详谈PHP垃圾回收机制
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
引用計數基本知識
每個php變量存在一個叫"zval"的變量容器中。一個zval變量容器,除了包含變量的類型和值,還包括兩個字節(jié)的額外信息。第一個是"is_ref",是個bool值,用來標識這個變量是否是屬于引用集合(reference set)。通過這個字節(jié),php引擎才能把普通變量和引用變量區(qū)分開來,由于php允許用戶通過使用&來使用自定義引用,zval變量容器中還有一個內部引用計數機制,來優(yōu)化內存使用。第二個額外字節(jié)是"refcount",用以表示指向這個zval變量容器的變量(也稱符號即symbol)個數。所有的符號存在一個符號表中,其中每個符號都有作用域(scope),那些主腳本(比如:通過瀏覽器請求的的腳本)和每個函數或者方法也都有作用域。
當一個變量被賦常量值時,就會生成一個zval變量容器,如下例這樣:
Example #1 生成一個新的zval容器
<?php
$a?=?"new?string";
?>
在上例中,新的變量a,是在當前作用域中生成的。并且生成了類型為string 和值為new string的變量容器。在額外的兩個字節(jié)信息中,"is_ref"被默認設置為FALSE,因為沒有任何自定義的引用生成。"refcount" 被設定為1,因為這里只有一個變量使用這個變量容器. 注意到當"refcount"的值是1時,"is_ref"的值總是FALSE. 如果你已經安裝了??Xdebug,你能通過調用函數xdebug_debug_zval()顯示"refcount"和"is_ref"的值。
Example #2 顯示zval信息
<?php
xdebug_debug_zval('a');
?>
以上例程會輸出:
a: (refcount=1, is_ref=0)='new string'
把一個變量賦值給另一變量將增加引用次數(refcount).
Example #3 增加一個zval的引用計數
<?php
$a?=?"new?string";
$b?=?$a;
xdebug_debug_zval(?'a'?);
?>
以上例程會輸出:
a: (refcount=2, is_ref=0)='new string'
這時,引用次數是2,因為同一個變量容器被變量a 和變量b關聯.當沒必要時,php不會去復制已生成的變量容器。變量容器在”refcount“變成0時就被銷毀. 當任何關聯到某個變量容器的變量離開它的作用域(比如:函數執(zhí)行結束),或者對變量調用了函數unset()時,”refcount“就會減1,下面的例子就能說明:
Example #4 減少引用計數
<?php
$a?=?"new?string";
$c?=?$b?=?$a;
xdebug_debug_zval(?'a'?);
unset(?$b,?$c?);
xdebug_debug_zval(?'a'?);
?>
以上例程會輸出:
a: (refcount=3, is_ref=0)='new string'a: (refcount=1, is_ref=0)='new string'
如果我們現在執(zhí)行 unset($a);,包含類型和值的這個變量容器就會從內存中刪除。
復合類型(Compound Types)
當考慮像 array和object這樣的復合類型時,事情就稍微有點復雜. 與 標量(scalar)類型的值不同,array和object類型的變量把它們的成員或屬性存在自己的符號表中。這意味著下面的例子將生成三個zval變量容器。
Example #5 Creating aarray zval
<?php
$a?=?array(?'meaning'?=>?'life',?'number'?=>?42?);
xdebug_debug_zval(?'a'?);
?>
以上例程的輸出類似于:
a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=1, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42)
圖示:
上面的結果如果在PHP5中是沒有問題的,但是當我在PHP7中進行試驗驗證是發(fā)現輸出的結果和上面并不一致,如下:
a:
(refcount=1, is_ref=0)array(size=2) 'meaning'=> (refcount=2, is_ref=0)string'life' (length=4) 'number'=> (refcount=0, is_ref=0)int42
這三個zval變量容器是:a,meaning和number。增加和減少”refcount”的規(guī)則和上面提到的一樣. 下面, 我們在數組中再添加一個元素,并且把它的值設為數組中已存在元素的值: Example #6 添加一個已經存在的元素到數組中 <?php $a?=?array(?'meaning'?=>?'life',?'number'?=>?42?); $a['life']?=?$a['meaning']; xdebug_debug_zval(?'a'?); ?> 以上例程的輸出類似于: a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=2, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42, 'life' => (refcount=2, is_ref=0)='life') PHP7中的運行結果 a: (refcount=1, is_ref=0)array(size=3) 'meaning'=> (refcount=3, is_ref=0)string'life' (length=4) 'number'=> (refcount=0, is_ref=0)int42 'life' =>(refcount=3, is_ref=0)string 'life' (length=4) 圖示: 從以上的xdebug輸出信息,我們看到原有的數組元素和新添加的數組元素關聯到同一個"refcount"2的zval變量容器. 盡管 Xdebug的輸出顯示兩個值為'life'的 zval 變量容器,其實是同一個。 函數xdebug_debug_zval()不顯示這個信息,但是你能通過顯示內存指針信息來看到。 刪除數組中的一個元素,就是類似于從作用域中刪除一個變量. 刪除后,數組中的這個元素所在的容器的“refcount”值減少,同樣,當“refcount”為0時,這個變量容器就從內存中被刪除,下面又一個例子可以說明: Example #7 從數組中刪除一個元素 <?php $a?=?array(?'meaning'?=>?'life',?'number'?=>?42?); $a['life']?=?$a['meaning']; unset(?$a['meaning'],?$a['number']?); xdebug_debug_zval(?'a'?); ?> 以上例程的輸出類似于: a: (refcount=1, is_ref=0)=array ( 'life' => (refcount=1, is_ref=0)='life') PHP7中運行的結果 a: (refcount=1, is_ref=0)array(size=1) 'life'=> (refcount=2, is_ref=0)string'life' (length=4) 現在,當我們添加一個數組本身作為這個數組的元素時,事情就變得有趣,下個例子將說明這個。例中我們加入了引用操作符,否則php將生成一個復制。 Example #8 把數組作為一個元素添加到自己 <?php $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)=...) PHP中運行的結果 a: (refcount=2, is_ref=1)array(size=2) 0=>(refcount=2, is_ref=0)string'one' (length=3) 1=> (refcount=2, is_ref=1)&array< 圖示: 能看到數組變量 (a) 同時也是這個數組的第二個元素(1) 指向的變量容器中“refcount”為2。上面的輸出結果中的"..."說明發(fā)生了遞歸操作, 顯然在這種情況下意味著"..."指向原始數組。 跟剛剛一樣,對一個變量調用unset,將刪除這個符號,且它指向的變量容器中的引用次數也減1。所以,如果我們在執(zhí)行完上面的代碼后,對變量$a調用unset, 那么變量$a 和數組元素 "1" 所指向的變量容器的引用次數減1, 從"2"變成"1". 下例可以說明: Example #9 Unsetting$a (refcount=1, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=...) 圖示:
通過PHP5和PHP7環(huán)境中的運行結果對比可以看出,PHP7中的內存回收機制有了改變,那么為什么會有這種改變呢?我查閱了一些資料供大家參考。 PHP7 中的 zval 在 PHP7 中 zval 有了新的實現方式。最基礎的變化就是 zval 需要的內存不再是單獨從堆上分配,不再自己存儲引用計數。復雜數據類型(比如字符串、數組和對象)的引用計數由其自身來存儲。這種實現方式有以下好處: 簡單數據類型不需要單獨分配內存,也不需要計數; 不會再有兩次計數的情況。在對象中,只有對象自身存儲的計數是有效的; 由于現在計數由數值自身存儲,所以也就可以和非 zval 結構的數據共享,比如 zval 和 hashtable key 之間; 間接訪問需要的指針數減少了。 我們看看現在 zval 結構體的定義(現在在 zend_types.h 文件中): struct _zval_struct { ?zend_value? value;?? /* value */ ?union { ??struct { ???ZEND_ENDIAN_LOHI_4( ????zend_uchar type,?? /* active type */ ????zend_uchar type_flags, ????zend_uchar const_flags, ????zend_uchar reserved)? /* call info for EX(This) */ ??} v; ??uint32_t type_info; ?} u1; ?union { ??uint32_t? var_flags; ??uint32_t? next;???? /* hash collision chain */ ??uint32_t? cache_slot;?? /* literal cache slot */ ??uint32_t? lineno;??? /* line number (for ast nodes) */ ??uint32_t? num_args;??? /* arguments number for EX(This) */ ??uint32_t? fe_pos;??? /* foreach position */ ??uint32_t? fe_iter_idx;?? /* foreach iterator index */ ?} u2; }; 結構體的第一個元素沒太大變化,仍然是一個 value 聯合體。第二個成員是由一個表示類型信息的整型和一個包含四個字符變量的結構體組成的聯合體(可以忽略 ZEND_ENDIAN_LOHI_4 宏,它只是用來解決跨平臺大小端問題的)。這個子結構中比較重要的部分是 type(和以前類似)和 type_flags,這個接下來會解釋。 上面這個地方也有一點小問題:value 本來應該占 8 個字節(jié),但是由于內存對齊,哪怕只增加一個字節(jié),實際上也是占用 16 個字節(jié)(使用一個字節(jié)就意味著需要額外的 8 個字節(jié))。但是顯然我們并不需要 8 個字節(jié)來存儲一個 type 字段,所以我們在 u1 的后面增加了了一個名為 u2 的聯合體。默認情況下是用不到的,需要使用的時候可以用來存儲 4 個字節(jié)的數據。這個聯合體可以滿足不同場景下的需求。 PHP7 中 value 的結構定義如下: typedef union _zend_value { ?zend_long?? lval;??? /* long value */ ?double?? dval;??? /* 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; } zend_value; 首先需要注意的是現在 value 聯合體需要的內存是 8 個字節(jié)而不是 16。它只會直接存儲整型(lval)或者浮點型(dval)數據,其他情況下都是指針(上面提到過,指針占用 8 個字節(jié),最下面的結構體由兩個 4 字節(jié)的無符號整型組成)。上面所有的指針類型(除了特殊標記的)都有一個同樣的頭(zend_refcounted)用來存儲引用計數: typedef struct _zend_refcounted_h { ?uint32_t?? refcount;?? /* reference counter 32-bit */ ?union { ??struct { ???ZEND_ENDIAN_LOHI_3( ????zend_uchar type, ????zend_uchar flags, /* used for strings & objects */ ????uint16_t? gc_info) /* keeps GC root number (or 0) and color */ ??} v; ??uint32_t type_info; ?} u; } zend_refcounted_h;
現在,這個結構體肯定會包含一個存儲引用計數的字段。除此之外還有 type、flags 和 gc_info。type 存儲的和 zval 中的 type 相同的內容,這樣 GC 在不存儲 zval 的情況下單獨使用引用計數。flags 在不同的數據類型中有不同的用途,這個放到下一部分講。 gc_info 和 PHP5 中的 buffered 作用相同,不過不再是位于根緩沖區(qū)的指針,而是一個索引數字。因為以前根緩沖區(qū)的大小是固定的(10000 個元素),所以使用一個 16 位(2 字節(jié))的數字代替 64 位(8 字節(jié))的指針足夠了。gc_info 中同樣包含一個『顏色』位用于回收時標記結點。 zval 內存管理 上文提到過 zval 需要的內存不再單獨從堆上分配。但是顯然總要有地方來存儲它,所以會存在哪里呢?實際上大多時候它還是位于堆中(所以前文中提到的地方重點不是堆,而是單獨分配),只不過是嵌入到其他的數據結構中的,比如 hashtable 和 bucket 現在就會直接有一個 zval 字段而不是指針。所以函數表編譯變量和對象屬性在存儲時會是一個 zval 數組并得到一整塊內存而不是散落在各處的 zval 指針。之前的 zval * 現在都變成了 zval。 之前當 zval 在一個新的地方使用時會復制一份 zval * 并增加一次引用計數。現在就直接復制 zval 的值(忽略 u2),某些情況下可能會增加其結構指針指向的引用計數(如果在進行計數)。 那么 PHP 怎么知道 zval 是否正在計數呢?不是所有的數據類型都能知道,因為有些類型(比如字符串或數組)并不是總需要進行引用計數。所以 type_info 字段就是用來記錄 zval 是否在進行計數的,這個字段的值有以下幾種情況: #define IS_TYPE_CONSTANT?? (1<<0) /* special */ #define IS_TYPE_IMMUTABLE?? (1<<1) /* special */ #define IS_TYPE_REFCOUNTED?? (1<<2) #define IS_TYPE_COLLECTABLE?? (1<<3) #define IS_TYPE_COPYABLE?? (1<<4) #define IS_TYPE_SYMBOLTABLE?? (1<<5) /* special */
注:在 7.0.0 的正式版本中,上面這一段宏定義的注釋這幾個宏是供 zval.u1.v.type_flags 使用的。這應該是注釋的錯誤,因為這個上述字段是 zend_uchar 類型。 type_info 的三個主要的屬性就是『可計數』(refcounted)、『可回收』(collectable)和『可復制』(copyable)。計數的問題上面已經提過了。『可回收』用于標記 zval 是否參與循環(huán),不如字符串通常是可計數的,但是你卻沒辦法給字符串制造一個循環(huán)引用的情況。 是否可復制用于表示在復制時是否需要在復制時制造(原文用的 "duplication" 來表述,用中文表達出來可能不是很好理解)一份一模一樣的實體。"duplication" 屬于深度復制,比如在復制數組時,不僅僅是簡單增加數組的引用計數,而是制造一份全新值一樣的數組。但是某些類型(比如對象和資源)即使 "duplication" 也只能是增加引用計數,這種就屬于不可復制的類型。這也和對象和資源現有的語義匹配(現有,PHP7 也是這樣,不單是 PHP5)。 下面的表格上標明了不同的類型會使用哪些標記(x 標記的都是有的特性)。『簡單類型』(simple types)指的是整型或布爾類型這些不使用指針指向一個結構體的類型。下表中也有『不可變』(immutable)的標記,它用來標記不可變數組的,這個在下一部分再詳述。 interned string(保留字符)在這之前沒有提過,其實就是函數名、變量名等無需計數、不可重復的字符串。 | refcounted | collectable | copyable | immutable ----------------+------------+-------------+----------+---------- simple types??? |??????????? |???????????? |????????? | string????????? |????? x???? |???????????? |???? x??? | interned string |??????????? |???????????? |????????? | array?????????? |????? x???? |????? x????? |???? x??? | immutable array |??????????? |???????????? |????????? |???? x object????????? |????? x???? |????? x????? |????????? | resource??????? |????? x???? |???????????? |????????? | reference?????? |????? x???? |???????????? |????????? | 要理解這一點,我們可以來看幾個例子,這樣可以更好的認識 zval 內存管理是怎么工作的。 下面是整數行為模式,在上文中 PHP5 的例子的基礎上進行了一些簡化 : <?php $a= 42; // $a = zval_1(type=IS_LONG, value=42) $b= $a; // $a = zval_1(type=IS_LONG, value=42) ???// $b = zval_2(type=IS_LONG, value=42) $a+= 1; // $a = zval_1(type=IS_LONG, value=43) ???// $b = zval_2(type=IS_LONG, value=42) unset($a); // $a = zval_1(type=IS_UNDEF) ???// $b = zval_2(type=IS_LONG, value=42) 這個過程其實挺簡單的。現在整數不再是共享的,變量直接就會分離成兩個單獨的 zval,由于現在 zval 是內嵌的所以也不需要單獨分配內存,所以這里的注釋中使用 = 來表示的而不是指針符號 ->,unset 時變量會被標記為 IS_UNDEF。下面看一下更復雜的情況: <?php $a= []; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[]) $b= $a; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=2, value=[]) ???// $b = zval_2(type=IS_ARRAY) ---^ // zval 分離在這里進行 $a[] = 1 // $a = zval_1(type=IS_ARRAY) -> zend_array_2(refcount=1, value=[1]) ???// $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[]) unset($a); // $a = zval_1(type=IS_UNDEF), zend_array_2 被銷毀 ???// $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[]) 這種情況下每個變量變量有一個單獨的 zval,但是是指向同一個(有引用計數) zend_array 的結構體。修改其中一個數組的值時才會進行復制。這點和 PHP5 的情況類似。 類型(Types) 我們大概看一下 PHP7 支持哪些類型(zval 使用的類型標記): /* regular data types */ #define IS_UNDEF???? 0 #define IS_NULL?????1 #define IS_FALSE???? 2 #define IS_TRUE????? 3 #define IS_LONG?????4 #define IS_DOUBLE????5 #define IS_STRING????6 #define IS_ARRAY????7 #define IS_OBJECT????8 #define IS_RESOURCE????9 #define IS_REFERENCE??? 10 /* constant expressions */ #define IS_CONSTANT???? 11 #define IS_CONSTANT_AST??? 12 /* internal types */ #define IS_INDIRECT???? 15 #define IS_PTR????? 17 這個列表和 PHP5 使用的類似,不過增加了幾項: IS_UNDEF 用來標記之前為 NULL 的 zval 指針(和 IS_NULL 并不沖突)。比如在上面的例子中使用 unset 注銷變量; IS_BOOL 現在分割成了 IS_FALSE 和 IS_TRUE 兩項。現在布爾類型的標記是直接記錄到 type 中,這么做可以優(yōu)化類型檢查。不過這個變化對用戶是透明的,還是只有一個『布爾』類型的數據(PHP 腳本中)。 PHP 引用不再使用 is_ref 來標記,而是使用 IS_REFERENCE 類型。這個也要放到下一部分講; IS_INDIRECT? 和? IS_PTR 是特殊的內部標記。 實際上上面的列表中應該還存在兩個 fake types,這里忽略了。 IS_LONG 類型表示的是一個 zend_long 的值,而不是原生的 C 語言的 long 類型。原因是 Windows 的 64 位系統(tǒng)(LLP64)上的 long 類型只有 32 位的位深度。所以 PHP5 在 Windows 上只能使用 32 位的數字。PHP7 允許你在 64 位的操作系統(tǒng)上使用 64 位的數字,即使是在 Windows 上面也可以。 zend_refcounted 的內容會在下一部分講。下面看看 PHP 引用的實現。 引用 PHP7 使用了和 PHP5 中完全不同的方法來處理 PHP & 符號引用的問題(這個改動也是 PHP7 開發(fā)過程中大量 bug 的根源)。我們先從 PHP5 中 PHP 引用的實現方式說起。 通常情況下, 寫時復制原則意味著當你修改一個 zval 之前需要對其進行分離來保證始終修改的只是某一個 PHP 變量的值。這就是傳值調用的含義。 但是使用 PHP 引用時這條規(guī)則就不適用了。如果一個 PHP 變量是 PHP 引用,就意味著你想要在將多個 PHP 變量指向同一個值。PHP5 中的 is_ref 標記就是用來注明一個 PHP 變量是不是 PHP 引用,在修改時需不需要進行分離的。比如: <?php $a= []; // $a? -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[]) $b=& $a; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[]) $b[] = 1; // $a = $b = zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[1]) ???// 因為 is_ref 的值是 1, 所以 PHP 不會對 zval 進行分離
但是這個設計的一個很大的問題在于它無法在一個 PHP 引用變量和 PHP 非引用變量之間共享同一個值。比如下面這種情況: <?php $a= []; // $a?? -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[]) $b= $a; // $a, $b? -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[]) $c= $b// $a, $b, $c -> zval_1(type=IS_ARRAY, refcount=3, is_ref=0) -> HashTable_1(value=[]) $d=& $c; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[]) ???// $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[]) ???// $d 是 $c 的引用, 但卻不是 $a 的 $b, 所以這里 zval 還是需要進行復制 ???// 這樣我們就有了兩個 zval, 一個 is_ref 的值是 0, 一個 is_ref 的值是 1. $d[] = 1; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[]) ???// $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[1]) ???// 因為有兩個分離了的 zval, $d[] = 1 的語句就不會修改 $a 和 $b 的值.
這種行為方式也導致在 PHP 中使用引用比普通的值要慢。比如下面這個例子: <?php $array= range(0, 1000000); $ref=& $array; var_dump(count($array)); // <-- 這里會進行分離
因為 count() 只接受傳值調用,但是 $array 是一個 PHP 引用,所以 count() 在執(zhí)行之前實際上會有一個對數組進行完整的復制的過程。如果 $array 不是引用,這種情況就不會發(fā)生了。 現在我們來看看 PHP7 中 PHP 引用的實現。因為 zval 不再單獨分配內存,也就沒辦法再使用和 PHP5 中相同的實現了。所以增加了一個 IS_REFERENCE 類型,并且專門使用 zend_reference 來存儲引用值: struct _zend_reference { ?zend_refcounted gc; ?zval??? val; }; 本質上 zend_reference 只是增加了引用計數的 zval。所有引用變量都會存儲一個 zval 指針并且被標記為 IS_REFERENCE。val 和其他的 zval 的行為一樣,尤其是它也可以在共享其所存儲的復雜變量的指針,比如數組可以在引用變量和值變量之間共享。 我們還是看例子,這次是 PHP7 中的語義。為了簡潔明了這里不再單獨寫出 zval,只展示它們指向的結構體: <?php $a= []; // $a????????? -> zend_array_1(refcount=1, value=[]) $b=& $a; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[]) $b[] = 1; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[1]) 上面的例子中進行引用傳遞時會創(chuàng)建一個 zend_reference,注意它的引用計數是 2(因為有兩個變量在使用這個 PHP 引用)。但是值本身的引用計數是 1(因為 zend_reference 只是有一個指針指向它)。下面看看引用和非引用混合的情況: <?php $a= []; // $a?? -> zend_array_1(refcount=1, value=[]) $b= $a; // $a, $b, -> zend_array_1(refcount=2, value=[]) $c= $b// $a, $b, $c -> zend_array_1(refcount=3, value=[]) $d=& $c; // $a, $b???????? -> zend_array_1(refcount=3, value=[]) ???// $c, $d -> zend_reference_1(refcount=2) ---^ ???// 注意所有變量共享同一個 zend_array, 即使有的是 PHP 引用有的不是 $d[] = 1; // $a, $b???????? -> zend_array_1(refcount=2, value=[]) ???// $c, $d -> zend_reference_1(refcount=2) -> zend_array_2(refcount=1, value=[1]) ???// 只有在這時進行賦值的時候才會對 zend_array 進行賦值
這里和 PHP5 最大的不同就是所有的變量都可以共享同一個數組,即使有的是 PHP 引用有的不是。只有當其中某一部分被修改的時候才會對數組進行分離。這也意味著使用 count() 時即使給其傳遞一個很大的引用數組也是安全的,不會再進行復制。不過引用仍然會比普通的數值慢,因為存在需要為 zend_reference 結構體分配內存(間接)并且引擎本身處理這一塊兒也不快的的原因。 結語 總結一下 PHP7 中最重要的改變就是 zval 不再單獨從堆上分配內存并且不自己存儲引用計數。需要使用 zval 指針的復雜類型(比如字符串、數組和對象)會自己存儲引用計數。這樣就可以有更少的內存分配操作、更少的間接指針使用以及更少的內存分配。
清理變量容器的問題(Cleanup Problems) 盡管不再有某個作用域中的任何符號指向這個結構(就是變量容器),由于數組元素“1”仍然指向數組本身,所以這個容器不能被清除 。因為沒有另外的符號指向它,用戶沒有辦法清除這個結構,結果就會導致內存泄漏。慶幸的是,php將在腳本執(zhí)行結束時清除這個數據結構,但是在php清除之前,將耗費不少內存。如果你要實現分析算法,或者要做其他像一個子元素指向它的父元素這樣的事情,這種情況就會經常發(fā)生。當然,同樣的情況也會發(fā)生在對象上,實際上對象更有可能出現這種情況,因為對象總是隱式的被引用。 如果上面的情況發(fā)生僅僅一兩次倒沒什么,但是如果出現幾千次,甚至幾十萬次的內存泄漏,這顯然是個大問題。這樣的問題往往發(fā)生在長時間運行的腳本中,比如請求基本上不會結束的守護進程(deamons)或者單元測試中的大的套件(sets)中。后者的例子:在給巨大的eZ(一個知名的PHP Library) 組件庫的模板組件做單元測試時,就可能會出現問題。有時測試可能需要耗用2GB的內存,而測試服務器很可能沒有這么大的內存。
回收周期(Collecting Cycles) 傳統(tǒng)上,像以前的 php 用到的引用計數內存機制,無法處理循環(huán)的引用內存泄漏。然而 5.3.0 PHP 使用文章??引用計數系統(tǒng)中的同步周期回收(Concurrent Cycle Collection in Reference Counted Systems)中的同步算法,來處理這個內存泄漏問題。 對算法的完全說明有點超出這部分內容的范圍,將只介紹其中基礎部分。首先,我們先要建立一些基本規(guī)則,如果一個引用計數增加,它將繼續(xù)被使用,當然就不再在垃圾中。如果引用計數減少到零,所在變量容器將被清除(free)。就是說,僅僅在引用計數減少到非零值時,才會產生垃圾周期(garbage cycle)。其次,在一個垃圾周期中,通過檢查引用計數是否減1,并且檢查哪些變量容器的引用次數是零,來發(fā)現哪部分是垃圾。 為避免不得不檢查所有引用計數可能減少的垃圾周期,這個算法把所有可能根(possible roots 都是zval變量容器),放在根緩沖區(qū)(root buffer)中(用紫色來標記,稱為疑似垃圾),這樣可以同時確保每個可能的垃圾根(possible garbage root)在緩沖區(qū)中只出現一次。僅僅在根緩沖區(qū)滿了時,才對緩沖區(qū)內部所有不同的變量容器執(zhí)行垃圾回收操作。看上圖的步驟 A。 在步驟 B 中,模擬刪除每個紫色變量。模擬刪除時可能將不是紫色的普通變量引用數減"1",如果某個普通變量引用計數變成0了,就對這個普通變量再做一次模擬刪除。每個變量只能被模擬刪除一次,模擬刪除后標記為灰(原文說確保不會對同一個變量容器減兩次"1",不對的吧)。 在步驟 C 中,模擬恢復每個紫色變量。恢復是有條件的,當變量的引用計數大于0時才對其做模擬恢復。同樣每個變量只能恢復一次,恢復后標記為黑,基本就是步驟 B 的逆運算。這樣剩下的一堆沒能恢復的就是該刪除的藍色節(jié)點了,在步驟 D 中遍歷出來真的刪除掉。 算法中都是模擬刪除、模擬恢復、真的刪除,都使用簡單的遍歷即可(最典型的深搜遍歷)。復雜度為執(zhí)行模擬操作的節(jié)點數正相關,不只是紫色的那些疑似垃圾變量。 現在,你已經對這個算法有了基本了解,我們回頭來看這個如何與PHP集成。默認的,PHP的垃圾回收機制是打開的,然后有個php.ini 設置允許你修改它:zend.enable_gc 。 當垃圾回收機制打開時,每當根緩存區(qū)存滿時,就會執(zhí)行上面描述的循環(huán)查找算法。根緩存區(qū)有固定的大小,可存10,000個可能根,當然你可以通過修改PHP源碼文件Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES,然后重新編譯PHP,來修改這個10,000值。當垃圾回收機制關閉時,循環(huán)查找算法永不執(zhí)行,然而,可能根將一直存在根緩沖區(qū)中,不管在配置中垃圾回收機制是否激活。 當垃圾回收機制關閉時,如果根緩沖區(qū)存滿了可能根,更多的可能根顯然不會被記錄。那些沒被記錄的可能根,將不會被這個算法來分析處理。如果他們是循環(huán)引用周期的一部分,將永不能被清除進而導致內存泄漏。 即使在垃圾回收機制不可用時,可能根也被記錄的原因是,相對于每次找到可能根后檢查垃圾回收機制是否打開而言,記錄可能根的操作更快。不過垃圾回收和分析機制本身要耗不少時間。 除了修改配置zend.enable_gc ,也能通過分別調用gc_enable() 和 gc_disable()函數來打開和關閉垃圾回收機制。調用這些函數,與修改配置項來打開或關閉垃圾回收機制的效果是一樣的。即使在可能根緩沖區(qū)還沒滿時,也能強制執(zhí)行周期回收。你能調用gc_collect_cycles()函數達到這個目的。這個函數將返回使用這個算法回收的周期數。 允許打開和關閉垃圾回收機制并且允許自主的初始化的原因,是由于你的應用程序的某部分可能是高時效性的。在這種情況下,你可能不想使用垃圾回收機制。當然,對你的應用程序的某部分關閉垃圾回收機制,是在冒著可能內存泄漏的風險,因為一些可能根也許存不進有限的根緩沖區(qū)。因此,就在你調用gc_disable()函數釋放內存之前,先調用gc_collect_cycles()函數可能比較明智。因為這將清除已存放在根緩沖區(qū)中的所有可能根,然后在垃圾回收機制被關閉時,可留下空緩沖區(qū)以有更多空間存儲可能根。
這三個zval變量容器是:a,meaning和number。增加和減少”refcount”的規(guī)則和上面提到的一樣. 下面, 我們在數組中再添加一個元素,并且把它的值設為數組中已存在元素的值: Example #6 添加一個已經存在的元素到數組中 <?php $a?=?array(?'meaning'?=>?'life',?'number'?=>?42?); $a['life']?=?$a['meaning']; xdebug_debug_zval(?'a'?); ?> 以上例程的輸出類似于: a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=2, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42, 'life' => (refcount=2, is_ref=0)='life') PHP7中的運行結果 a: (refcount=1, is_ref=0)array(size=3) 'meaning'=> (refcount=3, is_ref=0)string'life' (length=4) 'number'=> (refcount=0, is_ref=0)int42 'life' =>(refcount=3, is_ref=0)string 'life' (length=4) 圖示: 從以上的xdebug輸出信息,我們看到原有的數組元素和新添加的數組元素關聯到同一個"refcount"2的zval變量容器. 盡管 Xdebug的輸出顯示兩個值為'life'的 zval 變量容器,其實是同一個。 函數xdebug_debug_zval()不顯示這個信息,但是你能通過顯示內存指針信息來看到。 刪除數組中的一個元素,就是類似于從作用域中刪除一個變量. 刪除后,數組中的這個元素所在的容器的“refcount”值減少,同樣,當“refcount”為0時,這個變量容器就從內存中被刪除,下面又一個例子可以說明: Example #7 從數組中刪除一個元素 <?php $a?=?array(?'meaning'?=>?'life',?'number'?=>?42?); $a['life']?=?$a['meaning']; unset(?$a['meaning'],?$a['number']?); xdebug_debug_zval(?'a'?); ?> 以上例程的輸出類似于: a: (refcount=1, is_ref=0)=array ( 'life' => (refcount=1, is_ref=0)='life') PHP7中運行的結果 a: (refcount=1, is_ref=0)array(size=1) 'life'=> (refcount=2, is_ref=0)string'life' (length=4) 現在,當我們添加一個數組本身作為這個數組的元素時,事情就變得有趣,下個例子將說明這個。例中我們加入了引用操作符,否則php將生成一個復制。 Example #8 把數組作為一個元素添加到自己 <?php $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)=...) PHP中運行的結果 a: (refcount=2, is_ref=1)array(size=2) 0=>(refcount=2, is_ref=0)string'one' (length=3) 1=> (refcount=2, is_ref=1)&array< 圖示: 能看到數組變量 (a) 同時也是這個數組的第二個元素(1) 指向的變量容器中“refcount”為2。上面的輸出結果中的"..."說明發(fā)生了遞歸操作, 顯然在這種情況下意味著"..."指向原始數組。 跟剛剛一樣,對一個變量調用unset,將刪除這個符號,且它指向的變量容器中的引用次數也減1。所以,如果我們在執(zhí)行完上面的代碼后,對變量$a調用unset, 那么變量$a 和數組元素 "1" 所指向的變量容器的引用次數減1, 從"2"變成"1". 下例可以說明: Example #9 Unsetting$a (refcount=1, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=...) 圖示:
通過PHP5和PHP7環(huán)境中的運行結果對比可以看出,PHP7中的內存回收機制有了改變,那么為什么會有這種改變呢?我查閱了一些資料供大家參考。 PHP7 中的 zval 在 PHP7 中 zval 有了新的實現方式。最基礎的變化就是 zval 需要的內存不再是單獨從堆上分配,不再自己存儲引用計數。復雜數據類型(比如字符串、數組和對象)的引用計數由其自身來存儲。這種實現方式有以下好處: 簡單數據類型不需要單獨分配內存,也不需要計數; 不會再有兩次計數的情況。在對象中,只有對象自身存儲的計數是有效的; 由于現在計數由數值自身存儲,所以也就可以和非 zval 結構的數據共享,比如 zval 和 hashtable key 之間; 間接訪問需要的指針數減少了。 我們看看現在 zval 結構體的定義(現在在 zend_types.h 文件中): struct _zval_struct { ?zend_value? value;?? /* value */ ?union { ??struct { ???ZEND_ENDIAN_LOHI_4( ????zend_uchar type,?? /* active type */ ????zend_uchar type_flags, ????zend_uchar const_flags, ????zend_uchar reserved)? /* call info for EX(This) */ ??} v; ??uint32_t type_info; ?} u1; ?union { ??uint32_t? var_flags; ??uint32_t? next;???? /* hash collision chain */ ??uint32_t? cache_slot;?? /* literal cache slot */ ??uint32_t? lineno;??? /* line number (for ast nodes) */ ??uint32_t? num_args;??? /* arguments number for EX(This) */ ??uint32_t? fe_pos;??? /* foreach position */ ??uint32_t? fe_iter_idx;?? /* foreach iterator index */ ?} u2; }; 結構體的第一個元素沒太大變化,仍然是一個 value 聯合體。第二個成員是由一個表示類型信息的整型和一個包含四個字符變量的結構體組成的聯合體(可以忽略 ZEND_ENDIAN_LOHI_4 宏,它只是用來解決跨平臺大小端問題的)。這個子結構中比較重要的部分是 type(和以前類似)和 type_flags,這個接下來會解釋。 上面這個地方也有一點小問題:value 本來應該占 8 個字節(jié),但是由于內存對齊,哪怕只增加一個字節(jié),實際上也是占用 16 個字節(jié)(使用一個字節(jié)就意味著需要額外的 8 個字節(jié))。但是顯然我們并不需要 8 個字節(jié)來存儲一個 type 字段,所以我們在 u1 的后面增加了了一個名為 u2 的聯合體。默認情況下是用不到的,需要使用的時候可以用來存儲 4 個字節(jié)的數據。這個聯合體可以滿足不同場景下的需求。 PHP7 中 value 的結構定義如下: typedef union _zend_value { ?zend_long?? lval;??? /* long value */ ?double?? dval;??? /* 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; } zend_value; 首先需要注意的是現在 value 聯合體需要的內存是 8 個字節(jié)而不是 16。它只會直接存儲整型(lval)或者浮點型(dval)數據,其他情況下都是指針(上面提到過,指針占用 8 個字節(jié),最下面的結構體由兩個 4 字節(jié)的無符號整型組成)。上面所有的指針類型(除了特殊標記的)都有一個同樣的頭(zend_refcounted)用來存儲引用計數: typedef struct _zend_refcounted_h { ?uint32_t?? refcount;?? /* reference counter 32-bit */ ?union { ??struct { ???ZEND_ENDIAN_LOHI_3( ????zend_uchar type, ????zend_uchar flags, /* used for strings & objects */ ????uint16_t? gc_info) /* keeps GC root number (or 0) and color */ ??} v; ??uint32_t type_info; ?} u; } zend_refcounted_h;
現在,這個結構體肯定會包含一個存儲引用計數的字段。除此之外還有 type、flags 和 gc_info。type 存儲的和 zval 中的 type 相同的內容,這樣 GC 在不存儲 zval 的情況下單獨使用引用計數。flags 在不同的數據類型中有不同的用途,這個放到下一部分講。 gc_info 和 PHP5 中的 buffered 作用相同,不過不再是位于根緩沖區(qū)的指針,而是一個索引數字。因為以前根緩沖區(qū)的大小是固定的(10000 個元素),所以使用一個 16 位(2 字節(jié))的數字代替 64 位(8 字節(jié))的指針足夠了。gc_info 中同樣包含一個『顏色』位用于回收時標記結點。 zval 內存管理 上文提到過 zval 需要的內存不再單獨從堆上分配。但是顯然總要有地方來存儲它,所以會存在哪里呢?實際上大多時候它還是位于堆中(所以前文中提到的地方重點不是堆,而是單獨分配),只不過是嵌入到其他的數據結構中的,比如 hashtable 和 bucket 現在就會直接有一個 zval 字段而不是指針。所以函數表編譯變量和對象屬性在存儲時會是一個 zval 數組并得到一整塊內存而不是散落在各處的 zval 指針。之前的 zval * 現在都變成了 zval。 之前當 zval 在一個新的地方使用時會復制一份 zval * 并增加一次引用計數。現在就直接復制 zval 的值(忽略 u2),某些情況下可能會增加其結構指針指向的引用計數(如果在進行計數)。 那么 PHP 怎么知道 zval 是否正在計數呢?不是所有的數據類型都能知道,因為有些類型(比如字符串或數組)并不是總需要進行引用計數。所以 type_info 字段就是用來記錄 zval 是否在進行計數的,這個字段的值有以下幾種情況: #define IS_TYPE_CONSTANT?? (1<<0) /* special */ #define IS_TYPE_IMMUTABLE?? (1<<1) /* special */ #define IS_TYPE_REFCOUNTED?? (1<<2) #define IS_TYPE_COLLECTABLE?? (1<<3) #define IS_TYPE_COPYABLE?? (1<<4) #define IS_TYPE_SYMBOLTABLE?? (1<<5) /* special */
注:在 7.0.0 的正式版本中,上面這一段宏定義的注釋這幾個宏是供 zval.u1.v.type_flags 使用的。這應該是注釋的錯誤,因為這個上述字段是 zend_uchar 類型。 type_info 的三個主要的屬性就是『可計數』(refcounted)、『可回收』(collectable)和『可復制』(copyable)。計數的問題上面已經提過了。『可回收』用于標記 zval 是否參與循環(huán),不如字符串通常是可計數的,但是你卻沒辦法給字符串制造一個循環(huán)引用的情況。 是否可復制用于表示在復制時是否需要在復制時制造(原文用的 "duplication" 來表述,用中文表達出來可能不是很好理解)一份一模一樣的實體。"duplication" 屬于深度復制,比如在復制數組時,不僅僅是簡單增加數組的引用計數,而是制造一份全新值一樣的數組。但是某些類型(比如對象和資源)即使 "duplication" 也只能是增加引用計數,這種就屬于不可復制的類型。這也和對象和資源現有的語義匹配(現有,PHP7 也是這樣,不單是 PHP5)。 下面的表格上標明了不同的類型會使用哪些標記(x 標記的都是有的特性)。『簡單類型』(simple types)指的是整型或布爾類型這些不使用指針指向一個結構體的類型。下表中也有『不可變』(immutable)的標記,它用來標記不可變數組的,這個在下一部分再詳述。 interned string(保留字符)在這之前沒有提過,其實就是函數名、變量名等無需計數、不可重復的字符串。 | refcounted | collectable | copyable | immutable ----------------+------------+-------------+----------+---------- simple types??? |??????????? |???????????? |????????? | string????????? |????? x???? |???????????? |???? x??? | interned string |??????????? |???????????? |????????? | array?????????? |????? x???? |????? x????? |???? x??? | immutable array |??????????? |???????????? |????????? |???? x object????????? |????? x???? |????? x????? |????????? | resource??????? |????? x???? |???????????? |????????? | reference?????? |????? x???? |???????????? |????????? | 要理解這一點,我們可以來看幾個例子,這樣可以更好的認識 zval 內存管理是怎么工作的。 下面是整數行為模式,在上文中 PHP5 的例子的基礎上進行了一些簡化 : <?php $a= 42; // $a = zval_1(type=IS_LONG, value=42) $b= $a; // $a = zval_1(type=IS_LONG, value=42) ???// $b = zval_2(type=IS_LONG, value=42) $a+= 1; // $a = zval_1(type=IS_LONG, value=43) ???// $b = zval_2(type=IS_LONG, value=42) unset($a); // $a = zval_1(type=IS_UNDEF) ???// $b = zval_2(type=IS_LONG, value=42) 這個過程其實挺簡單的。現在整數不再是共享的,變量直接就會分離成兩個單獨的 zval,由于現在 zval 是內嵌的所以也不需要單獨分配內存,所以這里的注釋中使用 = 來表示的而不是指針符號 ->,unset 時變量會被標記為 IS_UNDEF。下面看一下更復雜的情況: <?php $a= []; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[]) $b= $a; // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=2, value=[]) ???// $b = zval_2(type=IS_ARRAY) ---^ // zval 分離在這里進行 $a[] = 1 // $a = zval_1(type=IS_ARRAY) -> zend_array_2(refcount=1, value=[1]) ???// $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[]) unset($a); // $a = zval_1(type=IS_UNDEF), zend_array_2 被銷毀 ???// $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=1, value=[]) 這種情況下每個變量變量有一個單獨的 zval,但是是指向同一個(有引用計數) zend_array 的結構體。修改其中一個數組的值時才會進行復制。這點和 PHP5 的情況類似。 類型(Types) 我們大概看一下 PHP7 支持哪些類型(zval 使用的類型標記): /* regular data types */ #define IS_UNDEF???? 0 #define IS_NULL?????1 #define IS_FALSE???? 2 #define IS_TRUE????? 3 #define IS_LONG?????4 #define IS_DOUBLE????5 #define IS_STRING????6 #define IS_ARRAY????7 #define IS_OBJECT????8 #define IS_RESOURCE????9 #define IS_REFERENCE??? 10 /* constant expressions */ #define IS_CONSTANT???? 11 #define IS_CONSTANT_AST??? 12 /* internal types */ #define IS_INDIRECT???? 15 #define IS_PTR????? 17 這個列表和 PHP5 使用的類似,不過增加了幾項: IS_UNDEF 用來標記之前為 NULL 的 zval 指針(和 IS_NULL 并不沖突)。比如在上面的例子中使用 unset 注銷變量; IS_BOOL 現在分割成了 IS_FALSE 和 IS_TRUE 兩項。現在布爾類型的標記是直接記錄到 type 中,這么做可以優(yōu)化類型檢查。不過這個變化對用戶是透明的,還是只有一個『布爾』類型的數據(PHP 腳本中)。 PHP 引用不再使用 is_ref 來標記,而是使用 IS_REFERENCE 類型。這個也要放到下一部分講; IS_INDIRECT? 和? IS_PTR 是特殊的內部標記。 實際上上面的列表中應該還存在兩個 fake types,這里忽略了。 IS_LONG 類型表示的是一個 zend_long 的值,而不是原生的 C 語言的 long 類型。原因是 Windows 的 64 位系統(tǒng)(LLP64)上的 long 類型只有 32 位的位深度。所以 PHP5 在 Windows 上只能使用 32 位的數字。PHP7 允許你在 64 位的操作系統(tǒng)上使用 64 位的數字,即使是在 Windows 上面也可以。 zend_refcounted 的內容會在下一部分講。下面看看 PHP 引用的實現。 引用 PHP7 使用了和 PHP5 中完全不同的方法來處理 PHP & 符號引用的問題(這個改動也是 PHP7 開發(fā)過程中大量 bug 的根源)。我們先從 PHP5 中 PHP 引用的實現方式說起。 通常情況下, 寫時復制原則意味著當你修改一個 zval 之前需要對其進行分離來保證始終修改的只是某一個 PHP 變量的值。這就是傳值調用的含義。 但是使用 PHP 引用時這條規(guī)則就不適用了。如果一個 PHP 變量是 PHP 引用,就意味著你想要在將多個 PHP 變量指向同一個值。PHP5 中的 is_ref 標記就是用來注明一個 PHP 變量是不是 PHP 引用,在修改時需不需要進行分離的。比如: <?php $a= []; // $a? -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[]) $b=& $a; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[]) $b[] = 1; // $a = $b = zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_1(value=[1]) ???// 因為 is_ref 的值是 1, 所以 PHP 不會對 zval 進行分離
但是這個設計的一個很大的問題在于它無法在一個 PHP 引用變量和 PHP 非引用變量之間共享同一個值。比如下面這種情況: <?php $a= []; // $a?? -> zval_1(type=IS_ARRAY, refcount=1, is_ref=0) -> HashTable_1(value=[]) $b= $a; // $a, $b? -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[]) $c= $b// $a, $b, $c -> zval_1(type=IS_ARRAY, refcount=3, is_ref=0) -> HashTable_1(value=[]) $d=& $c; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[]) ???// $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[]) ???// $d 是 $c 的引用, 但卻不是 $a 的 $b, 所以這里 zval 還是需要進行復制 ???// 這樣我們就有了兩個 zval, 一個 is_ref 的值是 0, 一個 is_ref 的值是 1. $d[] = 1; // $a, $b -> zval_1(type=IS_ARRAY, refcount=2, is_ref=0) -> HashTable_1(value=[]) ???// $c, $d -> zval_1(type=IS_ARRAY, refcount=2, is_ref=1) -> HashTable_2(value=[1]) ???// 因為有兩個分離了的 zval, $d[] = 1 的語句就不會修改 $a 和 $b 的值.
這種行為方式也導致在 PHP 中使用引用比普通的值要慢。比如下面這個例子: <?php $array= range(0, 1000000); $ref=& $array; var_dump(count($array)); // <-- 這里會進行分離
因為 count() 只接受傳值調用,但是 $array 是一個 PHP 引用,所以 count() 在執(zhí)行之前實際上會有一個對數組進行完整的復制的過程。如果 $array 不是引用,這種情況就不會發(fā)生了。 現在我們來看看 PHP7 中 PHP 引用的實現。因為 zval 不再單獨分配內存,也就沒辦法再使用和 PHP5 中相同的實現了。所以增加了一個 IS_REFERENCE 類型,并且專門使用 zend_reference 來存儲引用值: struct _zend_reference { ?zend_refcounted gc; ?zval??? val; }; 本質上 zend_reference 只是增加了引用計數的 zval。所有引用變量都會存儲一個 zval 指針并且被標記為 IS_REFERENCE。val 和其他的 zval 的行為一樣,尤其是它也可以在共享其所存儲的復雜變量的指針,比如數組可以在引用變量和值變量之間共享。 我們還是看例子,這次是 PHP7 中的語義。為了簡潔明了這里不再單獨寫出 zval,只展示它們指向的結構體: <?php $a= []; // $a????????? -> zend_array_1(refcount=1, value=[]) $b=& $a; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[]) $b[] = 1; // $a, $b -> zend_reference_1(refcount=2) -> zend_array_1(refcount=1, value=[1]) 上面的例子中進行引用傳遞時會創(chuàng)建一個 zend_reference,注意它的引用計數是 2(因為有兩個變量在使用這個 PHP 引用)。但是值本身的引用計數是 1(因為 zend_reference 只是有一個指針指向它)。下面看看引用和非引用混合的情況: <?php $a= []; // $a?? -> zend_array_1(refcount=1, value=[]) $b= $a; // $a, $b, -> zend_array_1(refcount=2, value=[]) $c= $b// $a, $b, $c -> zend_array_1(refcount=3, value=[]) $d=& $c; // $a, $b???????? -> zend_array_1(refcount=3, value=[]) ???// $c, $d -> zend_reference_1(refcount=2) ---^ ???// 注意所有變量共享同一個 zend_array, 即使有的是 PHP 引用有的不是 $d[] = 1; // $a, $b???????? -> zend_array_1(refcount=2, value=[]) ???// $c, $d -> zend_reference_1(refcount=2) -> zend_array_2(refcount=1, value=[1]) ???// 只有在這時進行賦值的時候才會對 zend_array 進行賦值
這里和 PHP5 最大的不同就是所有的變量都可以共享同一個數組,即使有的是 PHP 引用有的不是。只有當其中某一部分被修改的時候才會對數組進行分離。這也意味著使用 count() 時即使給其傳遞一個很大的引用數組也是安全的,不會再進行復制。不過引用仍然會比普通的數值慢,因為存在需要為 zend_reference 結構體分配內存(間接)并且引擎本身處理這一塊兒也不快的的原因。 結語 總結一下 PHP7 中最重要的改變就是 zval 不再單獨從堆上分配內存并且不自己存儲引用計數。需要使用 zval 指針的復雜類型(比如字符串、數組和對象)會自己存儲引用計數。這樣就可以有更少的內存分配操作、更少的間接指針使用以及更少的內存分配。
清理變量容器的問題(Cleanup Problems) 盡管不再有某個作用域中的任何符號指向這個結構(就是變量容器),由于數組元素“1”仍然指向數組本身,所以這個容器不能被清除 。因為沒有另外的符號指向它,用戶沒有辦法清除這個結構,結果就會導致內存泄漏。慶幸的是,php將在腳本執(zhí)行結束時清除這個數據結構,但是在php清除之前,將耗費不少內存。如果你要實現分析算法,或者要做其他像一個子元素指向它的父元素這樣的事情,這種情況就會經常發(fā)生。當然,同樣的情況也會發(fā)生在對象上,實際上對象更有可能出現這種情況,因為對象總是隱式的被引用。 如果上面的情況發(fā)生僅僅一兩次倒沒什么,但是如果出現幾千次,甚至幾十萬次的內存泄漏,這顯然是個大問題。這樣的問題往往發(fā)生在長時間運行的腳本中,比如請求基本上不會結束的守護進程(deamons)或者單元測試中的大的套件(sets)中。后者的例子:在給巨大的eZ(一個知名的PHP Library) 組件庫的模板組件做單元測試時,就可能會出現問題。有時測試可能需要耗用2GB的內存,而測試服務器很可能沒有這么大的內存。
回收周期(Collecting Cycles) 傳統(tǒng)上,像以前的 php 用到的引用計數內存機制,無法處理循環(huán)的引用內存泄漏。然而 5.3.0 PHP 使用文章??引用計數系統(tǒng)中的同步周期回收(Concurrent Cycle Collection in Reference Counted Systems)中的同步算法,來處理這個內存泄漏問題。 對算法的完全說明有點超出這部分內容的范圍,將只介紹其中基礎部分。首先,我們先要建立一些基本規(guī)則,如果一個引用計數增加,它將繼續(xù)被使用,當然就不再在垃圾中。如果引用計數減少到零,所在變量容器將被清除(free)。就是說,僅僅在引用計數減少到非零值時,才會產生垃圾周期(garbage cycle)。其次,在一個垃圾周期中,通過檢查引用計數是否減1,并且檢查哪些變量容器的引用次數是零,來發(fā)現哪部分是垃圾。 為避免不得不檢查所有引用計數可能減少的垃圾周期,這個算法把所有可能根(possible roots 都是zval變量容器),放在根緩沖區(qū)(root buffer)中(用紫色來標記,稱為疑似垃圾),這樣可以同時確保每個可能的垃圾根(possible garbage root)在緩沖區(qū)中只出現一次。僅僅在根緩沖區(qū)滿了時,才對緩沖區(qū)內部所有不同的變量容器執(zhí)行垃圾回收操作。看上圖的步驟 A。 在步驟 B 中,模擬刪除每個紫色變量。模擬刪除時可能將不是紫色的普通變量引用數減"1",如果某個普通變量引用計數變成0了,就對這個普通變量再做一次模擬刪除。每個變量只能被模擬刪除一次,模擬刪除后標記為灰(原文說確保不會對同一個變量容器減兩次"1",不對的吧)。 在步驟 C 中,模擬恢復每個紫色變量。恢復是有條件的,當變量的引用計數大于0時才對其做模擬恢復。同樣每個變量只能恢復一次,恢復后標記為黑,基本就是步驟 B 的逆運算。這樣剩下的一堆沒能恢復的就是該刪除的藍色節(jié)點了,在步驟 D 中遍歷出來真的刪除掉。 算法中都是模擬刪除、模擬恢復、真的刪除,都使用簡單的遍歷即可(最典型的深搜遍歷)。復雜度為執(zhí)行模擬操作的節(jié)點數正相關,不只是紫色的那些疑似垃圾變量。 現在,你已經對這個算法有了基本了解,我們回頭來看這個如何與PHP集成。默認的,PHP的垃圾回收機制是打開的,然后有個php.ini 設置允許你修改它:zend.enable_gc 。 當垃圾回收機制打開時,每當根緩存區(qū)存滿時,就會執(zhí)行上面描述的循環(huán)查找算法。根緩存區(qū)有固定的大小,可存10,000個可能根,當然你可以通過修改PHP源碼文件Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES,然后重新編譯PHP,來修改這個10,000值。當垃圾回收機制關閉時,循環(huán)查找算法永不執(zhí)行,然而,可能根將一直存在根緩沖區(qū)中,不管在配置中垃圾回收機制是否激活。 當垃圾回收機制關閉時,如果根緩沖區(qū)存滿了可能根,更多的可能根顯然不會被記錄。那些沒被記錄的可能根,將不會被這個算法來分析處理。如果他們是循環(huán)引用周期的一部分,將永不能被清除進而導致內存泄漏。 即使在垃圾回收機制不可用時,可能根也被記錄的原因是,相對于每次找到可能根后檢查垃圾回收機制是否打開而言,記錄可能根的操作更快。不過垃圾回收和分析機制本身要耗不少時間。 除了修改配置zend.enable_gc ,也能通過分別調用gc_enable() 和 gc_disable()函數來打開和關閉垃圾回收機制。調用這些函數,與修改配置項來打開或關閉垃圾回收機制的效果是一樣的。即使在可能根緩沖區(qū)還沒滿時,也能強制執(zhí)行周期回收。你能調用gc_collect_cycles()函數達到這個目的。這個函數將返回使用這個算法回收的周期數。 允許打開和關閉垃圾回收機制并且允許自主的初始化的原因,是由于你的應用程序的某部分可能是高時效性的。在這種情況下,你可能不想使用垃圾回收機制。當然,對你的應用程序的某部分關閉垃圾回收機制,是在冒著可能內存泄漏的風險,因為一些可能根也許存不進有限的根緩沖區(qū)。因此,就在你調用gc_disable()函數釋放內存之前,先調用gc_collect_cycles()函數可能比較明智。因為這將清除已存放在根緩沖區(qū)中的所有可能根,然后在垃圾回收機制被關閉時,可留下空緩沖區(qū)以有更多空間存儲可能根。
來源:http://blog.csdn.net/wangshiqueque/article/details/78074363
總結
以上是生活随笔為你收集整理的详谈PHP垃圾回收机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 诉求(说一说诉求的简介)
- 下一篇: PHP内核探索:新垃圾回收机制说明