新聞中心
本文將要講述 PHP 發(fā)展歷程中的垃圾回收及內存管理相關內容。

創(chuàng)新互聯建站長期為上千多家客戶提供的網站建設服務,團隊從業(yè)經驗10年,關注不同地域、不同群體,并針對不同對象提供差異化的產品和服務;打造開放共贏平臺,與合作伙伴共同營造健康的互聯網生態(tài)環(huán)境。為屏邊企業(yè)提供專業(yè)的網站制作、成都網站制作,屏邊網站改版等技術服務。擁有10多年豐富建站經驗和眾多成功案例,為您定制開發(fā)。
在 PHP 5.2 及以前的版本中,PHP 的垃圾回收采用的是 引用計數 算法。
引用計數基礎知識
引用計數基礎知識
php 的變量存儲在「zval」變量容器(數據結構)中,「zval」屬性包含如下信息:
- 當前變量的數據類型;
- 當前變量的值;
- 用于標識變量是否為引用傳遞的 is_ref 布爾類型標識;
- 指向該「zval」變量容器的變量個數的 refcount 標識符(即這個 zval 被引用的次數,注意這里的引用不是指引用傳值,注意區(qū)分)。
當一個變量被賦值時,就會生成一個對應的「zavl」變量容器?!就扑]學習:PHP視頻教程】
查看變量 zval 容器信息
要查看變量的「zval」容器信息(即查看變量的 is_ref 和 refcount),可以使用 XDebug 調試工具的 xdebug_debug_zval() 函數。
安裝 XDebug 擴展插件的方法可以查看 這個教程(https://github.com/huliuqing/phpnotes/issues/58),有關XDebug 使用方法請閱讀 官方文檔(https://xdebug.org/docs/)。
假設,我們已經成功安裝好 XDebug 工具,現在就可以來對變量進行調試了。
- 查看普通變量的 zval 信息
如果我們的 PHP 語句只是對變量進行簡單賦值時,is_ref 標識值為 0,refcount 值為 1;若將這個變量作為值賦值給另一個變量時,則增加 zval 變量容器的 refcount 計數;同理,銷毀(unset)變量時,「refcount」相應的減去 1。
請看下面的示例:
- 寫時復制
通過前面的簡單變量的 zval 信息我們知道 $copy 和 $name 共用 zval 變量容器(內存),然后通過 refcount 來表示當前這個 zval 被多少個變量使用。
看個實例:
注意到沒有,當將值 liugongzi handsome 賦值給變量 $copy 時,name 和 copy 的 refcount 值都變成了 1,在這個過程中發(fā)生以下幾個操作:
- 將 $copy 從 $name 的 zval(內從)中分離出來(即復制);
- 將 $name 的 refcount 減去 1;
- 對 $copy 的 zval 進行修改(重新賦值和修改 refcount);
這里只是簡單對「寫時復制」進行介紹,感興趣的朋友可以閱讀文末給出的參考資料進行更加深入的研究。
- 查看引用傳遞變量的 zval 信息
引用傳值(&)的「引用計數」規(guī)則同普通賦值語句一樣,只是 is_ref 標識的值為 1 表示該變量是引用傳值類型。
我們現在來看看引用傳值的示例:
- 復合類型的引用計數
與標量類型(整型、浮點型、布爾型等)不同,數組(array)和對象(object)這種符合類型的引用計數規(guī)則會稍復雜一些。
為了更好的說明,還是先看看數組的引用計數示例:
$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
上面的引用計數示意圖如下:
從圖中我們發(fā)現復合類型的引用計數規(guī)則基本上同標量的計數規(guī)則一樣,就給出的示例來說,PHP 會創(chuàng)建 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)
大致示意圖如下:
- 內存泄露
雖然,復合類型的引用計數規(guī)則同標量類型大致相同,但是如果引用的值為變量自身(即循環(huán)應用),在處理不當時,就有可能會造成內存泄露的問題。
讓我們來看看下面這個對數組進行引用傳值的示例:
從內存占用結果上看,雖然我們執(zhí)行了 unset($a) 方法來銷毀 $a 數組,但內存并沒有被回收,整個處理過程的示意圖如下:
可以看到對于這塊內存,再也沒有符合表(變量)指向了,所以 PHP 無法完成內存回收,官方給出的解釋如下:
簡單來說就是「引用計數」算法無法檢測并釋放循環(huán)引用所使用的內存,最終導致內存泄露。
引用計數系統的同步周期回收
由于引用計數算法存在無法回收循環(huán)應用導致的內存泄露問題,在 PHP 5.3 之后對內存回收的實現做了優(yōu)化,通過采用 引用計數系統的同步周期回收 算法實現內存管理。引用計數系統的同步周期回收算法是一個改良版本的引用計數算法,它在引用基礎上做出了如下幾個方面的增強:
- 引入了可能根(possible root)的概念:通過引用計數相關學習,我們知道如果一個變量(zval)被引用,要么是被全局符號表中的符號引用(即變量),要么被復雜類型(如數組)的 zval 中的符號(數組的元素)引用,那么這個 zval 變量容器就是「可能根」。
- 引入根緩沖區(qū)(root buffer)的概念:根緩沖區(qū)用于存放所有「可能根」,它是固定大小的,默認可存 10000 個可能根,如需修改可以通過修改 PHP 源碼文件 Zend/zend_gc.c 中的常量 GC_ROOT_BUFFER_MAX_ENTRIES,再重新編譯。
- 回收周期:當緩沖區(qū)滿時,對緩沖區(qū)中的所有可能根進行垃圾回收處理。
下圖(來自 PHP 手冊),展示了新的回收算法執(zhí)行過程:
引用計數系統的同步周期回收過程
- 緩沖區(qū)(紫色框部分,稱為疑似垃圾),存儲所有可能根(步驟 A);
- 采用深度優(yōu)先算法遍歷「根緩沖區(qū)」中所有的「可能根(即 zval 遍歷容器)」,并對每個 zval 的 refcount 減 1,為了避免遍歷時對同一個 zval 多次減 1(因為不同的根可能遍歷到同一個 zval)將這個 zvel 標記為「已減」(步驟 B);
- 再次采用深度優(yōu)先遍歷算法遍歷「可能根 zval」。當 zval 的 refcount 值不為 0 時,對其加 1,否則保持為 0。并請已遍歷的 zval 變量容器標記為「已恢復」(即步驟 B 的逆運算)。那些 zval 的 refcount 值為 0 (藍色框標記)的就是應該被回收的變量(步驟 C);
- 刪除所有 refcount 為 0 的可能根(步驟 D)。
整個過程為:
采用深度優(yōu)先算法執(zhí)行:默認刪除 > 模擬恢復 > 執(zhí)行刪除 達到內存回收的目的。
優(yōu)化后的引用計數算法優(yōu)勢
- 將內存泄露控制在閥值內,這個由緩存區(qū)實現,達到緩沖區(qū)大小執(zhí)行新一輪垃圾回收;
- 提升了垃圾回收性能,不是每次 refcount 減 1 都執(zhí)行回收處理,而是等到根緩沖區(qū)滿時才開始執(zhí)行垃圾回收。
你可以從 PHP 手冊 的回收周期 了解更多,也可以閱讀文末給出的參考資料。
PHP 7 的內存管理
PHP 5 中 zval 實現上的主要問題:
- zval 總是單獨 從堆中分配內存;
- zval 總是存儲引用計數和循環(huán)回收 的信息,即使是整型(bool / null)這種可能并不需要此類信息的數據;
- 在使用對象或者資源時,直接引用會導致兩次計數;
- 某些間接訪問需要一個更好的處理方式。比如現在訪問存儲在變量中的對象間接使用了四個指針(指針鏈的長度為四);
- 直接計數也就意味著數值只能在 zval 之間共享。如果想在 zval 和 hashtable key 之間共享一個字符串就不行(除非 hashtable key 也是 zval)。
PHP 7 中的 zval 數據結構實現的調整:
這種實現的優(yōu)勢:
- 簡單數據類型不需要單獨分配內存,也不需要計數;
- 不會再有兩次計數的情況。在對象中,只有對象自身存儲的計數是有效的;
- 由于現在計數由數值自身存儲(PHP 有 zval 變量容器存儲),所以也就可以和非 zval 結構的數據共享,比如 zval 和 hashtable key 之間;
- 間接訪問需要的指針數減少了。
網站標題:深入講解PHP垃圾回收及內存管理相關內容
文章地址:http://m.fisionsoft.com.cn/article/dpcgeio.html


咨詢
建站咨詢
