本篇文章給大家帶來的內容是關於PHP中記憶體回收與記憶體管理的詳細分析,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所協助。
引用計數
在 PHP 5.2 及以前的版本中,PHP 的記憶體回收採用的是 引用計數 演算法。
引用計數基礎知識
php 的變數儲存在「zval」變數容器(資料結構)中,「zval」屬性包含如下資訊:
當一個變數被賦值時,就會產生一個對應的「zavl」變數容器。
查看變數 zval 容器資訊
要查看變數的「zval」容器資訊(即查看變數的 is_ref 和 refcount),可以使用 XDebug 調試工具的 xdebug_debug_zval() 函數。
安裝 XDebug 擴充外掛程式的方法可以查看 這個教程,有關XDebug 使用方法請閱讀 官方文檔。
假設,我們已經成功安裝好 XDebug 工具,現在就可以來對變數進行調試了。
如果我們的 PHP 語句只是對變數進行簡單賦值時,is_ref 標識值為 0,refcount 值為 1;若將這個變數作為值賦值給另一個變數時,則增加 zval 變數容器的 refcount 計數;同理,銷毀(unset)變數時,「refcount」相應的減去 1。
請看下面的樣本:
<?php// 變數賦值時,refcount 值等於 1$name = 'liugongzi';xdebug_debug_zval('name'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9)// $name 作為值賦值給另一個變數, refcount 值增加 1$copy = $name;xdebug_debug_zval('name'); // (refcount=2, is_ref=0)string 'liugongzi' (length=9)// 銷毀變數,refcount 值減掉 1unset($copy);xdebug_debug_zval('name'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9)
寫時複製
寫時複製(Copy On Write:COW),簡單描述為:如果通過賦值的方式賦值給變數時不會申請新記憶體來存放新變數所儲存的值,而是簡單的通過一個計數器來共用記憶體,只有在其中的一個引用指向變數的值發生變化時,才申請新空間來儲存值內容以減少對記憶體的佔用。 - TPIP 寫時複製
通過前面的簡單變數的 zval 資訊我們知道 $copy 和 $name 共用 zval 變數容器(記憶體),然後通過 refcount 來表示當前這個 zval 被多少個變數使用。
看個執行個體:
<?php$name = 'liugongzi';xdebug_debug_zval('name'); // name: (refcount=1, is_ref=0)string 'liugongzi' (length=9)$copy = $name;xdebug_debug_zval('name'); // name: (refcount=2, is_ref=0)string 'liugongzi' (length=9)// 將新的值賦值給變數 $copy$copy = 'liugongzi handsome';xdebug_debug_zval('name'); // name: (refcount=1, is_ref=0)string 'liugongzi' (length=9)xdebug_debug_zval('copy'); // copy: (refcount=1, is_ref=0)='liugongzi handsome'
注意到沒有,當將值 liugongzi handsome 賦值給變數 $copy 時,name 和 copy 的 refcount 值都變成了 1,在這個過程中發生以下幾個操作:
將 $copy 從 $name 的 zval(內從)中分離出來(即複製);
將 $name 的 refcount 減去 1;
對 $copy 的 zval 進行修改(重新賦值和修改 refcount);
這裡只是簡單對「寫時複製」進行介紹,感興趣的朋友可以閱讀文末給出的參考資料進行更加深入的研究。
引用傳值(&)的「引用計數」規則同普通指派陳述式一樣,只是 is_ref 標識的值為 1 表示該變數是引用傳實值型別。
我們現在來看看引用傳值的樣本:
<?php$age = 'liugongzi';xdebug_debug_zval('age'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9)$copy = &$age;xdebug_debug_zval('age'); // (refcount=2, is_ref=1)string 'liugongzi' (length=9)unset($copy);xdebug_debug_zval('age'); // (refcount=1, is_ref=1)string 'liugongzi' (length=9)
與標量類型(整型、浮點型、布爾型等)不同,數組(array)和對象(object)這種符合類型的引用計數規則會稍複雜一些。
為了更好的說明,還是先看看數組的引用計數樣本:
$a = array( 'meaning' => 'life', 'number' => 42 );xdebug_debug_zval( 'a' );// a:// (refcount=1, is_ref=0)// array (size=2)// 'meaning' => (refcount=1, is_ref=0)string 'life' (length=4)// 'number' => (refcount=1, is_ref=0)int 42
上面的引用計數如下:
我們發現複合類型的引用計數規則基本上同標量的計數規則一樣,就給出的樣本來說,PHP 會建立 3 個 zval 變數容器,一個用於儲存數組本身,另外兩個用於儲存數組中的元素。
添加一個已經存在的元素到數組中時,它的引用計數器 refcount 會增加 1。
$a = array( 'meaning' => 'life', 'number' => 42 );xdebug_debug_zval( 'a' );$a['life'] = $a['meaning'];xdebug_debug_zval( 'a' );// a:// (refcount=1, is_ref=0)// array (size=3)// 'meaning' => (refcount=2, is_ref=0)string 'life' (length=4)// 'number' => (refcount=0, is_ref=0)int 42// 'life' => (refcount=2, is_ref=0)string 'life' (length=4)
大致如下:
雖然,複合類型的引用計數規則同標量類型大致相同,但是如果引用的值為變數自身(即迴圈應用),在處理不當時,就有可能會造成記憶體泄露的問題。
讓我們來看看下面這個對數組進行引用傳值的樣本:
<?php// @link http://php.net/manual/zh/function.memory-get-usage.php#96280function convert($size){ $unit=array('b','kb','mb','gb','tb','pb'); return @round($size/pow(1024,($i=floor(log($size,1024)))),2).' '.$unit[$i];}// 注意:有用的地方從這裡開始$memory = memory_get_usage();$a = array( 'one' );// 引用自身(循環參考)$a[] =&$a;xdebug_debug_zval( 'a' );var_dump(convert(memory_get_usage() - $memory)); // 296 bunset($a); // 刪除變數 $a,由於 $a 中的元素引用了自身(循環參考)最終導致 $a 所使用的記憶體無法被回收var_dump(convert(memory_get_usage() - $memory)); // 568 b
從記憶體佔用結果上看,雖然我們執行了 unset($a) 方法來銷毀 $a 數組,但記憶體並沒有被回收,整個處理過程的如下:
可以看到對於這塊記憶體,再也沒有符合表(變數)指向了,所以 PHP 無法完成記憶體回收,官方給出的解釋如下:
儘管不再有某個範圍中的任何符號指向這個結構 (就是變數容器),由於數組元素 “1” 仍然指向數組本身,所以這個容器不能被清除 。因為沒有另外的符號指向它,使用者沒有辦法清除這個結構,結果就會導致記憶體流失。慶幸的是,php 將在指令碼執行結束時清除這個資料結構,但是在 php 清除之前,將耗費不少記憶體。如果你要實現分析演算法,或者要做其他像一個子項目指向它的父元素這樣的事情,這種情況就會經常發生。當然,同樣的情況也會發生在對象上,實際上對象更有可能出現這種情況,因為對象總是隱式的被引用。
簡單來說就是「引用計數」演算法無法檢測並釋放循環參考所使用的記憶體,最終導致記憶體泄露。
引用計數系統的同步周期回收
由於引用計數演算法存在無法回收迴圈應用導致的記憶體泄露問題,在 PHP 5.3 之後對記憶體回收的實現做了最佳化,通過採用 引用計數系統的同步周期回收 演算法實現記憶體管理。引用計數系統的同步周期回收演算法是一個改良版本的引用計數演算法,它在引用基礎上做出了如下幾個方面的增強:
引入了可能根(possible root)的概念:通過引用計數相關學習,我們知道如果一個變數(zval)被引用,要麼是被全域符號表中的符號引用(即變數),要麼被複雜類型(如數組)的 zval 中的符號(數組的元素)引用,那麼這個 zval 變數容器就是「可能根」。
引入根緩衝區(root buffer)的概念:根緩衝區用於存放所有「可能根」,它是固定大小的,預設可存 10000 個可能根,如需修改可以通過修改 PHP 源碼檔案 Zend/zend_gc.c 中的常量 GC_ROOT_BUFFER_MAX_ENTRIES,再重新編譯。
回收周期:當緩衝區滿時,對緩衝區中的所有可能根進行記憶體回收處理。
(來自 PHP 手冊),展示了新的回收演算法執行過程:
引用計數系統的同步周期回收過程
緩衝區(紫色框部分,稱為疑似垃圾),儲存所有可能根(步驟 A);
採用深度優先演算法遍曆「根緩衝區」中所有的「可能根(即 zval 遍曆容器)」,並對每個 zval 的 refcount 減 1,為了避免遍曆時對同一個 zval 多次減 1(因為不同的根可能遍曆到同一個 zval)將這個 zvel 標記為「已減」(步驟 B);
再次採用深度優先遍曆演算法遍曆「可能根 zval」。當 zval 的 refcount 值不為 0 時,對其加 1,否則保持為 0。並請已遍曆的 zval 變數容器標記為「已恢複」(即步驟 B 的逆運算)。那些 zval 的 refcount 值為 0 (藍色框標記)的就是應該被回收的變數(步驟 C);
刪除所有 refcount 為 0 的可能根(步驟 D)。
整個過程為:
採用深度優先演算法執行:預設刪除 > 類比恢複 > 執行刪除 達到記憶體回收的目的。
最佳化後的引用計數演算法優勢
你可以從 PHP 手冊 的回收周期 瞭解更多,也可以閱讀文末給出的參考資料。
PHP 7 的記憶體管理
PHP 5 中 zval 實現上的主要問題:
zval 總是單獨 從堆中分配記憶體;
zval 總是儲存引用計數和迴圈回收 的資訊,即使是整型(bool / null)這種可能並不需要此類資訊的資料;
在使用對象或者資源時,直接引用會導致兩次計數;
某些間接訪問需要一個更好的處理方式。比如現在訪問儲存在變數中的對象間接使用了四個指標(指標鏈的長度為四);
直接計數也就意味著數值只能在 zval 之間共用。如果想在 zval 和 hashtable key 之間共用一個字串就不行(除非 hashtable key 也是 zval)。
PHP 7 中的 zval 資料結構實現的調整:
最基礎的變化就是 zval 需要的記憶體 不再是單獨從堆上分配,不再由 zval 儲存引用計數。
複雜資料類型(比如字串、數組和對象)的引用計數由其自身來儲存。
這種實現的優勢: