首頁  >  文章  >  後端開發  >  PHP中垃圾回收與記憶體管理的詳細分析

PHP中垃圾回收與記憶體管理的詳細分析

不言
不言原創
2018-08-15 09:40:551290瀏覽

這篇文章帶給大家的內容是關於PHP中垃圾回收與記憶體管理的詳細分析,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

引用計數

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

引用計數基礎

php 的變數儲存在「zval」變數容器(資料結構)中,「zval」屬性包含下列資訊:

  • 目前變數的資料型別;

  • 目前變數的值;

  • ##用於識別變數是否為引用傳遞的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。

請看下面的範例:

<?php // 变量赋值时,refcount 值等于 1
$name = &#39;liugongzi&#39;;
xdebug_debug_zval(&#39;name&#39;); // (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)

// $name 作为值赋值给另一个变量, refcount 值增加 1
$copy = $name;
xdebug_debug_zval(&#39;name&#39;); // (refcount=2, is_ref=0)string &#39;liugongzi&#39; (length=9)

// 销毁变量,refcount 值减掉 1
unset($copy);
xdebug_debug_zval(&#39;name&#39;); // (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)

#寫時複製

#寫時複製(Copy On Write:COW),簡單描述為:如果透過賦值的方式賦值給變數時不會申請新內存來存放新變數所保存的值,而是簡單的透過一個計數器來共用內存,只有在其中的一個引用指向變數的值改變時,才申請新空間來保存值內容以減少對記憶體的佔用。 - TPIP 寫時複製

透過前面的簡單變數的 zval 資訊我們知道 $copy 和 $name 共用 zval 變數容器(記憶體),然後透過 refcount 來表示目前這個 zval 被多少個變數使用。

看個實例:

<?php $name = &#39;liugongzi&#39;;
xdebug_debug_zval(&#39;name&#39;); // name: (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)

$copy = $name;
xdebug_debug_zval(&#39;name&#39;); // name: (refcount=2, is_ref=0)string &#39;liugongzi&#39; (length=9)

// 将新的值赋值给变量 $copy
$copy = &#39;liugongzi handsome&#39;;
xdebug_debug_zval(&#39;name&#39;); // name: (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)
xdebug_debug_zval(&#39;copy&#39;); // copy: (refcount=1, is_ref=0)=&#39;liugongzi handsome&#39;
注意到沒有,當值liugongzi handsome 賦值給變數$copy 時,name 和copy 的refcount 值都變成了1,在這個過程中發生以下幾個操作:

  • 將$copy 從$name 的zval(內從)中分離出來(即複製);

  • 將$name 的refcount 減去1;

  • 對$copy 的zval 進行修改(重新賦值和修改refcount);

#這裡只是簡單地對「寫時複製」介紹,有興趣的朋友可以閱讀文末給的參考資料進行更深入的研究。

  • 查看引用傳遞變數的zval 訊息

#引用傳值(&)的「引用計數」規則同普通賦值語句一樣,只是

is_ref 標識的值為1 表示該變數是引用傳值類型。

我們現在來看看引用傳值的範例:

<?php $age = &#39;liugongzi&#39;;
xdebug_debug_zval(&#39;age&#39;); // (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)

$copy = &$age;
xdebug_debug_zval(&#39;age&#39;); // (refcount=2, is_ref=1)string &#39;liugongzi&#39; (length=9)

unset($copy);
xdebug_debug_zval(&#39;age&#39;); // (refcount=1, is_ref=1)string &#39;liugongzi&#39; (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中垃圾回收與記憶體管理的詳細分析##從圖中我們發現複合型別的引用計數規則基本上是同標量的計數規則一樣,就給的範例來說,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中垃圾回收與記憶體管理的詳細分析

    #記憶體洩漏
  • 雖然,複合類型的引用計數規則同標量類型大致相同,但是如果引用的值為變數本身(即循環應用),在處理不當時,就有可能會造成記憶體外洩的問題。

讓我們來看看下面這個對陣列進行引用傳值的範例:

<?php // @link http://php.net/manual/zh/function.memory-get-usage.php#96280
function convert($size)
{
    $unit=array(&#39;b&#39;,&#39;kb&#39;,&#39;mb&#39;,&#39;gb&#39;,&#39;tb&#39;,&#39;pb&#39;);
    return @round($size/pow(1024,($i=floor(log($size,1024)))),2).&#39; &#39;.$unit[$i];
}

// 注意:有用的地方从这里开始
$memory = memory_get_usage();

$a = array( &#39;one&#39; );

// 引用自身(循环引用)
$a[] =&$a;

xdebug_debug_zval( &#39;a&#39; );

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中垃圾回收與記憶體管理的詳細分析

可以看到对于这块内存,再也没有符合表(变量)指向了,所以 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 手册),展示了新的回收算法执行过程:

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 儲存引用計數。
複雜資料類型(例如字串、陣列和物件)的參考計數由其自身來儲存。

這種實現的優點:

  • 簡單資料型別不需要單獨分配內存,也不需要計數;

  • ##不會再有兩次計數的情況。在物件中,只有物件本身儲存的計數是有效的;

  • 由於現在計數由數值自身儲存(PHP 有zval 變數容器儲存),所以也就可以和非zval 結構的資料共享,例如zval 和hashtable key 之間;

  • 間接存取所需的指標數減少了。

相關文章推薦:

PHP之腳本的記憶體管理與垃圾回收 - 個人文章思否

跟我學習javascript的垃圾回收機制與記憶體管理_javascript技巧

#垃圾回收 PHP垃圾回收機制簡單說明

以上是PHP中垃圾回收與記憶體管理的詳細分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn