本文首發於 PHP 垃圾回收與內存管理指引,轉載請註明出處。
本文將要講述 PHP 發展歷程中的垃圾回收及內存管理相關內容,文末給出 PHP 發展在各個階段有關內存管理及垃圾回收(內核)參考資料值得閱讀。php
在 PHP 5.2 及之前的版本中,PHP 的垃圾回收採用的是 引用計數 算法。html
引用計數基礎知識git
php 的變量存儲在「zval」變量容器(數據結構)中,「zval」屬性包含以下信息:github
當一個變量被賦值時,就會生成一個對應的「zavl」變量容器。算法
要查看變量的「zval」容器信息(即查看變量的 is_ref 和 refcount),可使用 XDebug 調試工具的 xdebug_debug_zval() 函數。數組
安裝 XDebug 擴展插件的方法能夠查看 這個教程,有關XDebug 使用方法請閱讀 官方文檔。緩存
假設,咱們已經成功安裝好 XDebug 工具,如今就能夠來對變量進行調試了。性能優化
若是咱們的 PHP 語句只是對變量進行簡單賦值時,is_ref 標識值爲 0,refcount 值爲 1;若將這個變量做爲值賦值給另外一個變量時,則增長 zval 變量容器的 refcount 計數;同理,銷燬(unset)變量時,「refcount」相應的減去 1。php7
請看下面的示例:數據結構
<?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 值減掉 1 unset($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,在這個過程當中發生如下幾個操做:
這裏只是簡單對「寫時複製」進行介紹,感興趣的朋友能夠閱讀文末給出的參考資料進行更加深刻的研究。
引用傳值(&)的「引用計數」規則同普通賦值語句同樣,只是 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#96280 function 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 b unset($a); // 刪除變量 $a,因爲 $a 中的元素引用了自身(循環引用)最終致使 $a 所使用的內存沒法被回收 var_dump(convert(memory_get_usage() - $memory)); // 568 b
從內存佔用結果上看,雖然咱們執行了 unset($a) 方法來銷燬 $a 數組,但內存並無被回收,整個處理過程的示意圖以下:
能夠看到對於這塊內存,再也沒有符合表(變量)指向了,因此 PHP 沒法完成內存回收,官方給出的解釋以下:
儘管再也不有某個做用域中的任何符號指向這個結構 (就是變量容器),因爲數組元素 「1」 仍然指向數組自己,因此這個容器不能被清除 。由於沒有另外的符號指向它,用戶沒有辦法清除這個結構,結果就會致使內存泄漏。慶幸的是,php 將在腳本執行結束時清除這個數據結構,可是在 php 清除以前,將耗費很多內存。若是你要實現分析算法,或者要作其餘像一個子元素指向它的父元素這樣的事情,這種狀況就會常常發生。固然,一樣的狀況也會發生在對象上,實際上對象更有可能出現這種狀況,由於對象老是隱式的被引用。 - 摘自 官方文檔 Cleanup Problems
簡單來講就是「引用計數」算法沒法檢測並釋放循環引用所使用的內存,最終致使內存泄露。
因爲引用計數算法存在沒法回收循環應用致使的內存泄露問題,在 PHP 5.3 以後對內存回收的實現作了優化,經過採用 引用計數系統的同步週期回收 算法實現內存管理。引用計數系統的同步週期回收算法是一個改良版本的引用計數算法,它在引用基礎上作出了以下幾個方面的加強:
下圖(來自 PHP 手冊),展現了新的回收算法執行過程:
整個過程爲:
採用深度優先算法執行:默認刪除 > 模擬恢復 > 執行刪除 達到內存回收的目的。
你能夠從 PHP 手冊 的回收週期 瞭解更多,也能夠閱讀文末給出的參考資料。
PHP 5 中 zval 實現上的主要問題:
PHP 7 中的 zval 數據結構實現的調整:
最基礎的變化就是 zval 須要的內存 再也不是單獨從堆上分配,再也不由 zval 存儲引用計數。
複雜數據類型(好比字符串、數組和對象)的引用計數由其自身來存儲。 - 摘自 Internal value representation in PHP 7 - Part 1【 譯】
這種實現的優點:
更具體的有關 PHP 7 zval 實現和內存優化細節能夠閱讀 深刻理解 PHP7 內核之 zval 和 Internal value representation in PHP 7 - Part 1譯。
Internal value representation in PHP 7 - Part 1【譯】
Internal value representation in PHP 7 - Part 2【譯】
淺談 PHP5 中垃圾回收算法 (Garbage Collection) 的演化
Confusion about PHP 7 refcount
引用計數系統中的同步週期回收 (Concurrent Cycle Collection in Reference Counted Systems) 論文