首頁 後端開發 php教程 淺談PHP5中垃圾回收演算法(Garbage Collection)的演化

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

Oct 17, 2016 am 10:50 AM

前言

PHP是一門託管型語言,在PHP程式設計中程式設計師不需要手動處理記憶體資源的分配與釋放(使用C編寫PHP或Zend擴充除外),這就意味著PHP本身實現了垃圾回收機制(Garbage Collection)。現在如果去PHP官方網站(php.net)可以看到,目前PHP5的兩個分支版本PHP5.2和PHP5.3是分別更新的,這是因為許多項目仍然使用5.2版本的PHP,而5.3版本對5.2並不是完全相容。 PHP5.3在PHP5.2的基礎上做了許多改進,其中垃圾回收演算法就屬於一個比較大的改變。本文將分別討論PHP5.2和PHP5.3的垃圾回收機制,並討論這種演化和改進對於程式設計師編寫PHP的影響以及要注意的問題。

PHP變數及關聯記憶體物件的內部表示

垃圾回收說到底是對變數及其所關聯記憶體物件的操作,所以在討論PHP的垃圾回收機制之前,先簡要介紹PHP中變數及其記憶體物件的內部表示(其C原始碼中的表示)。

PHP官方文件中將PHP中的變數分為兩類:標量類型和複雜類型。標量類型包括布林型、整數、浮點型和字串;複雜型別包括陣列、物件和資源;還有一個NULL比較特殊,它不分為任何型別,而是單獨成為一類。

所有這些類型,在PHP內部統一用一個叫做zval的結構表示,在PHP原始碼中這個結構名稱為「_zval_struct」。 zval的具體定義在PHP原始碼的「Zend/zend.h」檔案中,以下是相關程式碼的摘錄。

typedef union _zvalue_value {
    long lval;                  /* long value */
    double dval;                /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;              /* hash table value */
    zend_object_value obj;
} zvalue_value;
  
struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    zend_uint refcount__gc;
    zend_uchar type;    /* active type */
    zend_uchar is_ref__gc;
};
登入後複製

其中聯合體「_zvalue_value」用來表示PHP中所有變數的值,這裡之所以使用union,是因為一個zval在一個時刻只能表示一種類型的變數。可以看到_zvalue_value中只有5個字段,但是PHP中算上NULL有8種資料型,那麼PHP內部是如何用5個字段表示8種類型呢?這算是PHP設計比較巧妙的一個地方,它透過複用字段達到了減少字段的目的。例如,在PHP內部布林型、整數及資源(只要儲存資源的識別碼即可)都是透過lval欄位儲存的;dval用於儲存浮點型;str儲存字串;ht儲存陣列(注意PHP中的陣列其實是哈希表);而obj儲存物件類型;如果所有欄位全部置為0或NULL則表示PHP中的NULL,這樣就達到了用5個欄位儲存8種類型的值。

而當前zval中的value(value的類型即是_zvalue_value)到底表示那種類型,則由「_zval_struct」中的type決定。 _zval_struct即是zval在C語言中的具體實現,每個zval表示一個變數的記憶體物件。除了value和type,可以看到_zval_struct中還有兩個欄位refcount__gc和is_ref__gc,從其後綴可以斷定這兩個傢伙與垃圾回收有關。沒錯,PHP的垃圾回收全靠這倆欄位了。其中refcount__gc表示目前有幾個變數引用此zval,而is_ref__gc表示目前zval是否被按引用引用,這話聽起來很拗口,這和PHP中zval的「Write-On-Copy」機制有關,由於這個話題不是本文重點,因此這裡不再詳述,讀者只需記住refcount__gc這個字段的作用即可。

PHP5.2中的垃圾回收演算法——Reference Counting

PHP5.2中使用的記憶體回收演算法是大名鼎鼎的Reference Counting,這個演算法中文翻譯叫做“引用計數”,其思想非常直觀和簡潔:為每個記憶體對象分配一個計數器,當一個記憶體對象建立時計數器初始化為1(因此此時總是有一個變數引用此對象),以後每有一個新變數引用此記憶體對象,則計數器加1,而每當減少一個引用此記憶體物件的變數則計數器減1,當垃圾回收機制運作的時候,將所有計數器為0的記憶體物件銷毀並回收其佔用的記憶體。而PHP中記憶體物件就是zval,而計數器就是refcount__gc。

例如下面一段PHP程式碼示範了PHP5.2計數器的工作原理(計數器值透過xdebug得到):

<?php
  
$val1 = 100; //zval(val1).refcount_gc = 1;
$val2 = $val1; //zval(val1).refcount_gc = 2,zval(val2).refcount_gc = 2(因为是Write on copy,当前val2与val1共同引用一个zval)
$val2 = 200; //zval(val1).refcount_gc = 1,zval(val2).refcount_gc = 1(此处val2新建了一个zval)
unset($val1); //zval(val1).refcount_gc = 0($val1引用的zval再也不可用,会被GC回收)
  
?>
Reference Counting简单直观,实现方便,但却存在一个致命的缺陷,就是容易造成内存泄露。很多朋友可能已经意识到了,如果存在循环引用,那么Reference Counting就可能导致内存泄露。例如下面的代码:
<?php
$a = array();
$a[] = & $a;
unset($a);
  
?>
登入後複製

這段程式碼首先建立了數組a,然後讓a的第一個元素按引用指向a,這時a的zval的refcount就變成2,然後我們銷毀變數a,此時a最初指向的zval的refcount為1,但是我們再也沒有辦法對其進行操作,因為其形成了一個循環自引用,如下圖:

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

其中灰色部分表示已经不复存在。由于a之前指向的zval的refcount为1(被其HashTable的第一个元素引用),这个zval就不会被GC销毁,这部分内存就泄露了。

这里特别要指出的是,PHP是通过符号表(Symbol Table)存储变量符号的,全局有一个符号表,而每个复杂类型如数组或对象有自己的符号表,因此上面代码中,a和a[0]是两个符号,但是a储存在全局符号表中,而a[0]储存在数组本身的符号表中,且这里a和a[0]引用同一个zval(当然符号a后来被销毁了)。希望读者朋友注意分清符号(Symbol)的zval的关系。

在PHP只用于做动态页面脚本时,这种泄露也许不是很要紧,因为动态页面脚本的生命周期很短,PHP会保证当脚本执行完毕后,释放其所有资源。但是PHP发展到目前已经不仅仅用作动态页面脚本这么简单,如果将PHP用在生命周期较长的场景中,例如自动化测试脚本或deamon进程,那么经过多次循环后积累下来的内存泄露可能就会很严重。这并不是我在耸人听闻,我曾经实习过的一个公司就通过PHP写的deamon进程来与数据存储服务器交互。

由于Reference Counting的这个缺陷,PHP5.3改进了垃圾回收算法。

PHP5.3中的垃圾回收算法——Concurrent Cycle Collection in Reference Counted Systems

PHP5.3的垃圾回收算法仍然以引用计数为基础,但是不再是使用简单计数作为回收准则,而是使用了一种同步回收算法,这个算法由IBM的工程师在论文Concurrent Cycle Collection in Reference Counted Systems中提出。

这个算法可谓相当复杂,从论文29页的数量我想大家也能看出来,所以我不打算(也没有能力)完整论述此算法,有兴趣的朋友可以阅读上面的提到的论文(强烈推荐,这篇论文非常精彩)。

我在这里,只能大体描述一下此算法的基本思想。

首先PHP会分配一个固定大小的“根缓冲区”,这个缓冲区用于存放固定数量的zval,这个数量默认是10,000,如果需要修改则需要修改源代码Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES然后重新编译。

由上文我们可以知道,一个zval如果有引用,要么被全局符号表中的符号引用,要么被其它表示复杂类型的zval中的符号引用。因此在zval中存在一些可能根(root)。这里我们暂且不讨论PHP是如何发现这些可能根的,这是个很复杂的问题,总之PHP有办法发现这些可能根zval并将它们投入根缓冲区。

当根缓冲区满额时,PHP就会执行垃圾回收,此回收算法如下:

1、对每个根缓冲区中的根zval按照深度优先遍历算法遍历所有能遍历到的zval,并将每个zval的refcount减1,同时为了避免对同一zval多次减1(因为可能不同的根能遍历到同一个zval),每次对某个zval减1后就对其标记为“已减”。

2、再次对每个缓冲区中的根zval深度优先遍历,如果某个zval的refcount不为0,则对其加1,否则保持其为0。

3、清空根缓冲区中的所有根(注意是把这些zval从缓冲区中清除而不是销毁它们),然后销毁所有refcount为0的zval,并收回其内存。

如果不能完全理解也没有关系,只需记住PHP5.3的垃圾回收算法有以下几点特性:

1、并不是每次refcount减少时都进入回收周期,只有根缓冲区满额后在开始垃圾回收。

2、可以解决循环引用问题。

3、可以总将内存泄露保持在一个阈值以下。

PHP5.2与PHP5.3垃圾回收算法的性能比较

由于我目前条件所限,我就不重新设计试验了,而是直接引用PHP Manual中的实验,关于两者的性能比较请参考PHP Manual中的相关章节:http://www.php.net/manual/en/features.gc.performance-considerations.php。

首先是内存泄露试验,下面直接引用PHP Manual中的实验代码和试验结果图:

<?php
class Foo
{
    public $var = &#39;3.1415962654&#39;;
}
  
$baseMemory = memory_get_usage();
  
for ( $i = 0; $i <= 100000; $i++ )
{
    $a = new Foo;
    $a->self = $a;
    if ( $i % 500 === 0 )
    {
        echo sprintf( &#39;%8d: &#39;, $i ), memory_get_usage() - $baseMemory, "\n";
    }
}
?>
登入後複製

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

PHP内存泄露试验

可以看到在可能引发累积性内存泄露的场景下,PHP5.2发生持续累积性内存泄露,而PHP5.3则总能将内存泄露控制在一个阈值以下(与根缓冲区大小有关)。

另外是关于性能方面的对比:

<?php
class Foo
{
    public $var = &#39;3.1415962654&#39;;
}
  
for ( $i = 0; $i <= 1000000; $i++ )
{
    $a = new Foo;
    $a->self = $a;
}
  
echo memory_get_peak_usage(), "\n";
?>
登入後複製

这个脚本执行1000000次循环,使得延迟时间足够进行对比。

然后使用CLI方式分别在打开内存回收和关闭内存回收的的情况下运行此脚本:

time php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php
# and
time php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php
登入後複製

在我的机器环境下,运行时间分别为6.4s和7.2s,可以看到PHP5.3的垃圾回收机制会慢一些,但是影响并不大。

与垃圾回收算法相关的PHP配置

可以通过修改php.ini中的zend.enable_gc来打开或关闭PHP的垃圾回收机制,也可以通过调用gc_enable()或gc_disable()打开或关闭PHP的垃圾回收机制。在PHP5.3中即使关闭了垃圾回收机制,PHP仍然会记录可能根到根缓冲区,只是当根缓冲区满额时,PHP不会自动运行垃圾回收,当然,任何时候您都可以通过手工调用gc_collect_cycles()函数强制执行内存回收。


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

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

php中的捲曲:如何在REST API中使用PHP捲曲擴展 php中的捲曲:如何在REST API中使用PHP捲曲擴展 Mar 14, 2025 am 11:42 AM

PHP客戶端URL(curl)擴展是開發人員的強大工具,可以與遠程服務器和REST API無縫交互。通過利用Libcurl(備受尊敬的多協議文件傳輸庫),PHP curl促進了有效的執行

在Codecanyon上的12個最佳PHP聊天腳本 在Codecanyon上的12個最佳PHP聊天腳本 Mar 13, 2025 pm 12:08 PM

您是否想為客戶最緊迫的問題提供實時的即時解決方案? 實時聊天使您可以與客戶進行實時對話,並立即解決他們的問題。它允許您為您的自定義提供更快的服務

解釋PHP中晚期靜態結合的概念。 解釋PHP中晚期靜態結合的概念。 Mar 21, 2025 pm 01:33 PM

文章討論了PHP 5.3中介紹的PHP中的晚期靜態結合(LSB),允許靜態方法的運行時間分辨率調用以更靈活的繼承。 LSB的實用應用和潛在的觸摸

在PHP API中說明JSON Web令牌(JWT)及其用例。 在PHP API中說明JSON Web令牌(JWT)及其用例。 Apr 05, 2025 am 12:04 AM

JWT是一種基於JSON的開放標準,用於在各方之間安全地傳輸信息,主要用於身份驗證和信息交換。 1.JWT由Header、Payload和Signature三部分組成。 2.JWT的工作原理包括生成JWT、驗證JWT和解析Payload三個步驟。 3.在PHP中使用JWT進行身份驗證時,可以生成和驗證JWT,並在高級用法中包含用戶角色和權限信息。 4.常見錯誤包括簽名驗證失敗、令牌過期和Payload過大,調試技巧包括使用調試工具和日誌記錄。 5.性能優化和最佳實踐包括使用合適的簽名算法、合理設置有效期、

框架安全功能:防止漏洞。 框架安全功能:防止漏洞。 Mar 28, 2025 pm 05:11 PM

文章討論了框架中的基本安全功能,以防止漏洞,包括輸入驗證,身份驗證和常規更新。

自定義/擴展框架:如何添加自定義功能。 自定義/擴展框架:如何添加自定義功能。 Mar 28, 2025 pm 05:12 PM

本文討論了將自定義功能添加到框架上,專注於理解體系結構,識別擴展點以及集成和調試的最佳實踐。

如何用PHP的cURL庫發送包含JSON數據的POST請求? 如何用PHP的cURL庫發送包含JSON數據的POST請求? Apr 01, 2025 pm 03:12 PM

使用PHP的cURL庫發送JSON數據在PHP開發中,經常需要與外部API進行交互,其中一種常見的方式是使用cURL庫發送POST�...

See all articles