PHP 垃圾回收與內存管理指引

圖片描述

本文首發於 PHP 垃圾回收與內存管理指引,轉載請註明出處。

本文將要講述 PHP 發展歷程中的垃圾回收及內存管理相關內容,文末給出 PHP 發展在各個階段有關內存管理及垃圾回收(內核)參考資料值得閱讀。php

引用計數

在 PHP 5.2 及之前的版本中,PHP 的垃圾回收採用的是 引用計數 算法。html

引用計數基礎知識

引用計數基礎知識git

php 的變量存儲在「zval」變量容器(數據結構)中,「zval」屬性包含以下信息:github

  • 當前變量的數據類型;
  • 當前變量的值;
  • 用於標識變量是否爲引用傳遞的 is_ref 布爾類型標識;
  • 指向該「zval」變量容器的變量個數的 refcount 標識符(即這個 zval 被引用的次數,注意這裏的引用不是指引用傳值,注意區分)。

當一個變量被賦值時,就會生成一個對應的「zavl」變量容器。算法

查看變量 zval 容器信息

要查看變量的「zval」容器信息(即查看變量的 is_ref 和 refcount),可使用 XDebug 調試工具的 xdebug_debug_zval() 函數。數組

安裝 XDebug 擴展插件的方法能夠查看 這個教程,有關XDebug 使用方法請閱讀 官方文檔緩存

假設,咱們已經成功安裝好 XDebug 工具,如今就能夠來對變量進行調試了。性能優化

  • 查看普通變量的 zval 信息

若是咱們的 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 信息咱們知道 &dollar;copy&dollar;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 賦值給變量 &dollar;copy 時,name 和 copy 的 refcount 值都變成了 1,在這個過程當中發生如下幾個操做:

  • 將 &dollar;copy 從 &dollar;name 的 zval(內從)中分離出來(即複製);
  • 將 &dollar;name 的 refcount 減去 1;
  • 對 &dollar;copy 的 zval 進行修改(從新賦值和修改 refcount);

這裏只是簡單對「寫時複製」進行介紹,感興趣的朋友能夠閱讀文末給出的參考資料進行更加深刻的研究。

  • 查看引用傳遞變量的 zval 信息

引用傳值(&)的「引用計數」規則同普通賦值語句同樣,只是 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(&dollar;a) 方法來銷燬 &dollar;a 數組,但內存並無被回收,整個處理過程的示意圖以下:

示意圖

能夠看到對於這塊內存,再也沒有符合表(變量)指向了,因此 PHP 沒法完成內存回收,官方給出的解釋以下:

儘管再也不有某個做用域中的任何符號指向這個結構 (就是變量容器),因爲數組元素 「1」 仍然指向數組自己,因此這個容器不能被清除 。由於沒有另外的符號指向它,用戶沒有辦法清除這個結構,結果就會致使內存泄漏。慶幸的是,php 將在腳本執行結束時清除這個數據結構,可是在 php 清除以前,將耗費很多內存。若是你要實現分析算法,或者要作其餘像一個子元素指向它的父元素這樣的事情,這種狀況就會常常發生。固然,一樣的狀況也會發生在對象上,實際上對象更有可能出現這種狀況,由於對象老是隱式的被引用。 - 摘自 官方文檔 Cleanup Problems

簡單來講就是「引用計數」算法沒法檢測並釋放循環引用所使用的內存,最終致使內存泄露。

引用計數系統的同步週期回收

因爲引用計數算法存在沒法回收循環應用致使的內存泄露問題,在 PHP 5.3 以後對內存回收的實現作了優化,經過採用 引用計數系統的同步週期回收 算法實現內存管理。引用計數系統的同步週期回收算法是一個改良版本的引用計數算法,它在引用基礎上作出了以下幾個方面的加強:

  • 引入了可能根(possible root)的概念:經過引用計數相關學習,咱們知道若是一個變量(zval)被引用,要麼是被全局符號表中的符號引用(即變量),要麼被複雜類型(如數組)的 zval 中的符號(數組的元素)引用,那麼這個 zval 變量容器就是「可能根」。
  • 引入根緩衝區(root buffer)的概念:根緩衝區用於存放全部「可能根」,它是固定大小的,默承認存 10000 個可能根,如需修改能夠經過修改 PHP 源碼文件 Zend/zend_gc.c 中的常量 GC_ROOT_BUFFER_MAX_ENTRIES,再從新編譯。
  • 回收週期:當緩衝區滿時,對緩衝區中的全部可能根進行垃圾回收處理。

下圖(來自 PHP 手冊),展現了新的回收算法執行過程:

回收週期

引用計數系統的同步週期回收過程

  1. 緩衝區(紫色框部分,稱爲疑似垃圾),存儲全部可能根(步驟 A);
  2. 採用深度優先算法遍歷「根緩衝區」中全部的「可能根(即 zval 遍歷容器)」,並對每一個 zval 的 refcount 減 1,爲了不遍歷時對同一個 zval 屢次減 1(由於不一樣的根可能遍歷到同一個 zval)將這個 zvel 標記爲「已減」(步驟 B);
  3. 再次採用深度優先遍歷算法遍歷「可能根 zval」。當 zval 的 refcount 值不爲 0 時,對其加 1,不然保持爲 0。並請已遍歷的 zval 變量容器標記爲「已恢復」(即步驟 B 的逆運算)。那些 zval 的 refcount 值爲 0 (藍色框標記)的就是應該被回收的變量(步驟 C);
  4. 刪除全部 refcount 爲 0 的可能根(步驟 D)。

整個過程爲:

採用深度優先算法執行:默認刪除 > 模擬恢復 > 執行刪除 達到內存回收的目的。

優化後的引用計數算法優點

  • 將內存泄露控制在閥值內,這個由緩存區實現,達到緩衝區大小執行新一輪垃圾回收;
  • 提高了垃圾回收性能,不是每次 refcount 減 1 都執行回收處理,而是等到根緩衝區滿時纔開始執行垃圾回收。

你能夠從 PHP 手冊 的回收週期 瞭解更多,也能夠閱讀文末給出的參考資料。

PHP 7 的內存管理

PHP 5 中 zval 實現上的主要問題:

  • zval 老是單獨 從堆中分配內存;
  • zval 老是存儲引用計數和循環回收 的信息,即便是整型(bool / null)這種可能並不須要此類信息的數據;
  • 在使用對象或者資源時,直接引用會致使兩次計數;
  • 某些間接訪問須要一個更好的處理方式。好比如今訪問存儲在變量中的對象間接使用了四個指針(指針鏈的長度爲四);
  • 直接計數也就意味着數值只能在 zval 之間共享。若是想在 zval 和 hashtable key 之間共享一個字符串就不行(除非 hashtable key 也是 zval)。

PHP 7 中的 zval 數據結構實現的調整:

最基礎的變化就是 zval 須要的內存 再也不是單獨從堆上分配,再也不由 zval 存儲引用計數。
複雜數據類型(好比字符串、數組和對象)的引用計數由其自身來存儲。 - 摘自 Internal value representation in PHP 7 - Part 1

這種實現的優點:

  • 簡單數據類型不須要單獨分配內存,也不須要計數;
  • 不會再有兩次計數的狀況。在對象中,只有對象自身存儲的計數是有效的;
  • 因爲如今計數由數值自身存儲(PHP 有 zval 變量容器存儲),因此也就能夠和非 zval 結構的數據共享,好比 zval 和 hashtable key 之間;
  • 間接訪問須要的指針數減小了。

更具體的有關 PHP 7 zval 實現和內存優化細節能夠閱讀 深刻理解 PHP7 內核之 zvalInternal value representation in PHP 7 - Part 1

參考資料

深刻理解 PHP7 內核之 zval

Internal value representation in PHP 7 - Part 1

Internal value representation in PHP 7 - Part 2

TPIP:第六節 寫時複製(Copy On Write)

TPIP:內存管理

PHP7 內核之 zval

淺談 PHP5 中垃圾回收算法 (Garbage Collection) 的演化

Confusion about PHP 7 refcount

引用計數系統中的同步週期回收 (Concurrent Cycle Collection in Reference Counted Systems) 論文

PHP7 革新與性能優化

相關文章
相關標籤/搜索