1. 程式人生 > >PHP5底層原理之垃圾回收機制

PHP5底層原理之垃圾回收機制

概念

垃圾回收機制 是一種記憶體動態分配的方案,它會自動釋放程式不再使用的已分配的記憶體塊。

垃圾回收機制 可以讓程式設計師不必過分關心程式記憶體分配,從而將更多的精力投入到業務邏輯。

與之相關的一個概念,記憶體洩露 指的是程式未能釋放那些已經不再使用的記憶體,造成記憶體的浪費。

那麼 PHP 是如何實現垃圾回收機制的呢?

PHP變數的內部儲存結構

首先還是需要了解下 基礎知識,便於對垃圾回收原理內容的理解。

PHP 所有型別的變數在底層都會以 zval 結構體 的形式實現 (原始碼檔案Zend/zend.h)

原始碼根目錄搜尋

grep -rin --color --include=*.h --include=*.c _zval_struct *

struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* 變數value值 */
    zend_uint refcount__gc; /* 引用計數記憶體中使用次數,為0刪除該變數 */
    zend_uchar type;    /* 變數型別 */
    zend_uchar is_ref__gc; /* 區分是否是引用變數,是引用為1,否則為0 */
};

注:上面 zval 結構體是 php5.3 版本之後的結構,php5.3 之前因為沒有引入新的垃圾回收機制,即 GC,所以命名也沒有_gc

;而 php7 版本之後由於效能問題所以改寫了 zval 結構,這裡不再表述。

引用計數原理

變數容器

每個 PHP 變數存於一個叫 zval 的變數容器中。建立變數容器時,變數容器的 ref_count 初始值為 1, 每次被變數使用後,ref_count + 1 。當刪除變數時(unset( )),則它指向的變數容器的 ref_count - 1 。

非 array 和 object 變數

每次將常量賦值給一個變數時,都會產生 一個 變數容器

舉例:

$a = 'new string';
xdebug_debug_zval('a');

結果會輸出:

a:(refcount=1, is_ref=0),string 'new string' (length=10)

array 和 object 變數

每次將常量賦值給一個變數時,都會產生 元素個數 +1 個 變數容器

舉例:

$b = [
    'name' => 'new string',
    'number' => 12
];
xdebug_debug_zval('b');

結果會輸出:

b:
(refcount=1, is_ref=0),
array (size=2)
  'name' => (refcount=1, is_ref=0),string 'new string' (length=10)
  'number' => (refcount=1, is_ref=0),int 12

賦值原理

寫時複製原理

php 在設計的時候,為了節省記憶體,所以在變數之間賦值時,對於值相同的兩個變數,會共用一塊記憶體,也就是會在 全域性符號表 內將變數 b 的變數指標指向變數 a 指向的同一個 zval 結構體,而只有當其中一個變數的 zval 結構發生變化時,才會發生變數容器複製的記憶體變化,也因此叫做 寫時複製原理。

寫時複製原理 觸發時機:

php在修改一個變數時,如果發現變數的 refcount > 1,則會執行變數容器的記憶體複製

舉例:

// 建立一個變數容器,變數 a 指向給變數容器,a 的 ref_count 為 1
$a = ['name' => 'string','number' => 3];    

// 變數 b 也指向變數 a 指向的變數容器,a 和 b 的 ref_count 為 2
$b = $a;    
xdebug_debug_zval('a', 'b');
echo '<hr/>'
// 變數 b 的其中一個元素髮生改變,此時會複製出一個新的變數容器,變數 b 重新指向新的變數容器,a 和 b 的ref_count 變成 1
$b['name'] = 'new string';  
xdebug_debug_zval('a', 'b'); 

結果輸出:

a:(refcount=2, is_ref=0),
array (size=2)
  'name' => (refcount=1, is_ref=0),string 'string' (length=6)
  'number' => (refcount=1, is_ref=0),int 3
b:(refcount=2, is_ref=0),
array (size=2)
  'name' => (refcount=1, is_ref=0),string 'string' (length=6)
  'number' => (refcount=1, is_ref=0),int 3
________________________________________________________________________________________  
a:(refcount=1, is_ref=0),
array (size=2)
  'name' => (refcount=1, is_ref=0),string 'string' (length=6)
  'number' => (refcount=2, is_ref=0),int 3
b:(refcount=1, is_ref=0),
array (size=2)
  'name' => (refcount=1, is_ref=0),string 'new string' (length=10)
  'number' => (refcount=2, is_ref=0),int 3

 

寫時改變原理

上面說了普通賦值的情況,那麼將引用賦值呢?

先通過舉例說明

$a = ['name' => 'string','number' => 3];    
$b = &$a;
xdebug_debug_zval("a", "b");

結果輸出

a:(refcount=2, is_ref=1),
array (size=2)
  'name' => (refcount=1, is_ref=0),string 'string' (length=6)
  'number' => (refcount=1, is_ref=0),int 3
b:(refcount=2, is_ref=1),
array (size=2)
  'name' => (refcount=1, is_ref=0),string 'string' (length=6)
  'number' => (refcount=1, is_ref=0),int 3

此時,我們發現,變數 a 和 b 的 refcount 還是 2,只不過 is_ref 變成了 1,那是因為在將變數 a 引用賦值給變數b 時,在原變數容器上作了修改,將 is_ref 變成了 1,且 refcount + 1

那如果引用賦值的基礎上又發生了變數的改變了呢?

$a = ['name' => 'string','number' => 3];    
$b = &$a;
$b['name'] = "new string";
xdebug_debug_zval("a", "b");

結果輸出:

a:(refcount=2, is_ref=1),
array (size=2)
  'name' => (refcount=1, is_ref=0),string 'new string' (length=10)
  'number' => (refcount=1, is_ref=0),int 3
b:(refcount=2, is_ref=1),
array (size=2)
  'name' => (refcount=1, is_ref=0),string 'new string' (length=10)
  'number' => (refcount=1, is_ref=0),int 3

神奇的事情發生了,變數 b 和變數 a 的值一起發生改變了,其實這是因為觸發了寫時改變原理。

寫時改變原理 觸發時機:
is_ref 為 1 的變數容器在被賦值之前,優先檢查變數容器的 is_ref 是否等於 1 ,如果為 1,則不進行寫時複製,而是在原變數容器基礎上作內容修改;而如果將 is_ref 為 1 的變數容器賦值給其他變數時,則會立即觸發 寫時改變原理

現在將上面幾個例子結合起來,又會是怎樣的呢?

$a = ['name' => 'string','number' => 3];    
$b = $a;
$c = &$a;
xdebug_debug_zval("a", "b", "c");

結果輸出:

執行過程:

執行第一行:變數容器的 refcount 為 1

執行第二行:變數容器的 refcount 為 2,變數 a 和 變數 b 共享同一個變數容器

執行第三行:要將變數 a 引用賦值 給 變數 c,此時變數容器的 refcount > 1,如果要發生改變,會觸發 寫時複製,將變數 a 和 變數 b 分離,之後將變數 a 引用賦值給變數 c,則變數容器的 is_rel 變成 1,且 refcount 變成 2。

引用計數清 0

當變數容器的 ref_count 計數清 0 時,表示該變數容器就會被銷燬,實現了記憶體回收。

這就是 PHP 5.3 版本之前的垃圾回收機制。

舉例:

$a = "new string";
$b = $a;
xdebug_debug_zval('a');
unset($b);      // 刪除了符號表中的變數名 b,同時它指向的變數容器 ref_count -1
xdebug_debug_zval('a');
xdebug_debug_zval('b');

結果輸出:

a:(refcount=2, is_ref=0),string 'new string' (length=10)
a:(refcount=1, is_ref=0),string 'new string' (length=10)
b: no such symbol

迴圈引用引發的記憶體洩露問題

當我們新增一個 陣列或物件 作為這個 陣列或物件 的元素時,而如果此時刪除了這個變數符號(unset),此變數容器並不會被刪除。因為其子元素還在指向該變數容器,但是由於所有作用域內沒有任何符號指向這個變數容器,所以使用者沒有辦法清除這個變數容器,結果就會導致記憶體洩露,直到該指令碼執行結束被動清除這個變數容器。

舉例:把陣列作為一個元素新增到自己

$a = array( 'one' );
$a[] = &$a;
xdebug_debug_zval( 'a' );

會輸出:

a:
(refcount=2, is_ref=1),
array (size=2)
  0 => (refcount=1, is_ref=0),string 'one' (length=3)
  1 => (refcount=2, is_ref=1),&array<

圖示:

能看到陣列變數 a 同時也是這個陣列的第二個元素「1」指向的變數容器中 refcount 為 2。上面的輸出結果中的 &array< 意味著指向原始陣列。

跟剛剛一樣,對一個變數呼叫 unset,將刪除這個符號,且它指向的變數容器中的引用次數也減 1。所以,如果我們在執行完上面的程式碼後,對變數 a 呼叫 unset , 那麼變數 ​ a 和陣列元素 「1」所指向的變數容器的引用次數減 1, 從 2 變成了 1 . 下例可以說明:

unset($a);

圖示:

如果上面的情況發生僅僅一兩次倒沒什麼,但是如果出現幾千次,甚至幾十萬次的記憶體洩漏,這顯然是個大問題。這樣的問題往往發生在長時間執行的指令碼中,比如請求基本上不會結束的守護程序(deamons)或者單元測試中的大的套件(sets)中。

新的垃圾回收機制

PHP 5.3 版本之後引入 根緩衝機制,即 PHP 啟動時預設設定指定 zval 數量的根緩衝區(預設是10000),當 PHP發現有存在 迴圈引用 的 zval 時,就會把其投入到根緩衝區,當根緩衝區達到配置檔案中的指定數量(預設是10000)後,就會進行垃圾回收,以此解決迴圈引用導致的記憶體洩漏問題。

垃圾回收演算法

每當根快取區存滿時,PHP 會對根緩衝區的所有變數容器遍歷進行 模擬刪除,然後進行 模擬恢復。但是 PHP 只會對進行模擬刪除後 refcount > 0 的變數容器進行恢復,那麼沒有進行恢復的也就是 refcount = 0 的就是垃圾了。

確認為垃圾的準則

1、如果引用計數減少到零,所在變數容器將被清除(free),不屬於垃圾
2、如果一個zval 的引用計數減少後還大於0,那麼它會進入垃圾週期。其次,在一個垃圾週期中,通過檢查引用計數是否減1,並且檢查哪些變數容器的引用次數是零,來發現哪部分是垃圾。

總結

垃圾回收機制:
1、以 php 的引用計數機制為基礎( php5.3 以前只有該機制)
2、同時使用根緩衝區機制,當 php 發現有存在迴圈引用的 zval 時,就會把其投入到根緩衝區,當根緩衝區達到配置檔案中的指定數量後,就會進行垃圾回收,以此解決迴圈引用導致的記憶體洩漏問題( php5.3 開始引入該機制)

參考資料

PHP進階學習之垃圾回收機制詳解

php底層原理之垃圾回收機制

引用計數基本