php新垃圾回收機制說明

在5.2及更早版本的PHP中,沒有專門的垃圾回收器GC(Garbage Collection),引擎在判斷一個變量空間是否可以被釋放的時候是依據這個變量的zval的refcount的值,若是refcount爲0,那麼變量的空間能夠被釋放,不然就不釋放,這是一種很是簡單的GC實現。然而在這種簡單的GC實現方案中,出現了意想不到的變量內存泄漏狀況(Bug:http://bugs.php.net/bug.php?id=33595),引擎將沒法回收這些內存,因而在PHP5.3中出現了新的GC,新的GC有專門的機制負責清理垃圾數據,防止內存泄漏。本文將詳細的闡述PHP5.3中新的GC運行機制。php

目前不多有詳細的資料介紹新的GC,本文將是目前國內最爲詳細的從源碼角度介紹PHP5.3中GC原理的文章。其中關於垃圾產生以及算法簡介部分由筆者根據手冊翻譯而來,固然其中融入了本人的一些見解。手冊中相關內容:Garbage Collection算法

什麼算垃圾

首先咱們須要定義一下「垃圾」的概念,新的GC負責清理的垃圾是指變量的容器zval還存在,可是又沒有任何變量名指向此zval。所以GC判斷是否爲垃圾的一個重要標準是有沒有變量名指向變量容器zval。數組

假設咱們有一段PHP代碼,使用了一個臨時變量$tmp存儲了一個字符串,在處理完字符串以後,就不須要這個$tmp變量了,$tmp變量對於咱們來講能夠算是一個「垃圾」了,可是對於GC來講,$tmp其實並非一個垃圾,$tmp變量對咱們沒有意義,可是這個變量實際還存在,$tmp符號依然指向它所對應的zval,GC會認爲PHP代碼中可能還會使用到此變量,因此不會將其定義爲垃圾。函數

那麼若是咱們在PHP代碼中使用完$tmp後,調用unset刪除這個變量,那麼$tmp是否是就成爲一個垃圾了呢。很惋惜,GC仍然不認爲$tmp是一個垃圾,由於$tmp在unset以後,refcount減小1變成了0(這裏假設沒有別的變量和$tmp指向相同的zval),這個時候GC會直接將$tmp對應的zval的內存空間釋放,$tmp和其對應的zval就根本不存在了。此時的$tmp也不是新的GC所要對付的那種「垃圾」。那麼新的GC究竟要對付什麼樣的垃圾呢,下面咱們將生產一個這樣的垃圾。  性能

頑固垃圾的產生過程

若是讀者已經閱讀了變量內部存儲相關的內容,想必對refcount和isref這些變量內部的信息有了必定的瞭解。這裏咱們將結合手冊中的一個例子來介紹垃圾的產生過程:測試

<?php
$a = "new string";
?>

在這麼簡單的一個代碼中,$a變量內部存儲信息爲:a: (refcount=1, is_ref=0)='new string'spa

當把$a賦值給另一個變量的時候,$a對應的zval的refcount會加1。.net

<?php
$a = "new string";
$b = $a;
?>

此時$a和$b變量對應的內部存儲信息爲 a,b: (refcount=2, is_ref=0)='new string'翻譯

當咱們用unset刪除$b變量的時候,$b對應的zval的refcount會減小1code

<?php
$a = "new string"; 	//a: (refcount=1, is_ref=0)='new string'
$b = $a;       		//a,b: (refcount=2, is_ref=0)='new string'
unset($b);      	//a: (refcount=1, is_ref=0)='new string'
?>

   對於普通的變量來講,這一切彷佛很正常,可是在複合類型變量(數組和對象)中,會發生比較有意思的事情:

<?php
$a = array('meaning' => 'life', 'number' => 42);
?>

a的內部存儲信息爲:

a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=1, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42
)

數組變量自己($a)在引擎內部其實是一個哈希表,這張表中有兩個zval項 meaning和number,因此實際上那一行代碼中一共生成了3個zval,這3個zval都遵循變量的引用和計數原則,用圖來表示:

下面在$a中添加一個元素,並將現有的一個元素的值賦給新的元素:

<?php
$a = array('meaning' => 'life', 'number' => 42);
$a['life'] = $a['meaning'];
?>

那麼$a的內部存儲爲:

a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=2, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42,
   'life' => (refcount=2, is_ref=0)='life'
)

其中的meaning元素和life元素之指向同一個zval的:

如今,若是咱們試一下,將數組的引用賦值給數組中的一個元素,有意思的事情就發生了:

<?php
$a = array('one');
$a[] = &$a;
?>

這樣$a數組就有兩個元素,一個索引爲0,值爲字符one,另一個索引爲1,爲$a自身的引用,內部存儲以下:

a: (refcount=2, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=2, is_ref=1)=...
)

「...」表示1指向a自身,是一個環形引用:

這個時候咱們對$a進行unset,那麼$a會從符號表中刪除,同時$a指向的zval的refcount減小1

<?php
$a = array('one');
$a[] = &$a;
unset($a);
?>

那麼問題也就產生了,$a已經不在符號表中了,用戶沒法再訪問此變量,可是$a以前指向的zval的refcount變爲1而不是0,所以不能被回收,這樣產生了內存泄露:

這樣,這麼一個zval就成爲了一個真是意義的垃圾了,新的GC要作的工做就是清理這種垃圾。

新的GC算法

爲解決這種垃圾,產生了新的GC。

在PHP5.3版本中,使用了專門GC機制清理垃圾,在以前的版本中是沒有專門的GC,那麼垃圾產生的時候,沒有辦法清理,內存就白白浪費掉了。在PHP5.3源代碼中多瞭如下文件:{PHPSRC}/Zend/zend_gc.h {PHPSRC}/Zend/zend_gc.c, 這裏就是新的GC的實現,咱們先簡單的介紹一下算法思路,而後再從源碼的角度詳細介紹引擎中如何實現這個算法的。

在較新的PHP手冊中有簡單的介紹新的GC使用的垃圾清理算法,這個算法名爲 Concurrent Cycle Collection in Reference Counted Systems , 這裏不詳細介紹此算法,根據手冊中的內容來先簡單的介紹一下思路:

首先咱們有幾個基本的準則:

  1. 若是一個zval的refcount增長,那麼此zval還在使用,不屬於垃圾

  2. 若是一個zval的refcount減小到0, 那麼zval能夠被釋放掉,不屬於垃圾

  3. 若是一個zval的refcount減小以後大於0,那麼此zval還不能被釋放,此zval可能成爲一個垃圾

只有在準則3下,GC纔會把zval收集起來,而後經過新的算法來判斷此zval是否爲垃圾。那麼如何判斷這麼一個變量是否爲真正的垃圾呢?

簡單的說,就是對此zval中的每一個元素進行一次refcount減1操做,操做完成以後,若是zval的refcount=0,那麼這個zval就是一個垃圾。這個原理咋看起來很簡單,可是又不是那麼容易理解,起初筆者也沒法理解其含義,直到挖掘了源代碼以後纔算是瞭解。若是你如今不理解沒有關係,後面會詳細介紹,這裏先把這算法的幾個步驟描敘一下,首先引用手冊中的一張圖:

  • A:爲了不每次變量的refcount減小的時候都調用GC的算法進行垃圾判斷,此算法會先把全部前面準則3狀況下的zval節點放入一個節點(root)緩衝區(root buffer),而且將這些zval節點標記成紫色,同時算法必須確保每個zval節點在緩衝區中之出現一次。當緩衝區被節點塞滿的時候,GC纔開始開始對緩衝區中的zval節點進行垃圾判斷。

  • B:當緩衝區滿了以後,算法以深度優先對每個節點所包含的zval進行減1操做,爲了確保不會對同一個zval的refcount重複執行減1操做,一旦zval的refcount減1以後會將zval標記成灰色。須要強調的是,這個步驟中,起初節點zval自己不作減1操做,可是若是節點zval中包含的zval又指向了節點zval(環形引用),那麼這個時候須要對節點zval進行減1操做。

  • C:算法再次以深度優先判斷每個節點包含的zval的值,若是zval的refcount等於0,那麼將其標記成白色(表明垃圾),若是zval的refcount大於0,那麼將對此zval以及其包含的zval進行refcount加1操做,這個是對非垃圾的還原操做,同時將這些zval的顏色變成黑色(zval的默認顏色屬性)。

  • D:遍歷zval節點,將C中標記成白色的節點zval釋放掉。

這ABCD四個過程是手冊中對這個算法的介紹,這還不是那麼容易理解其中的原理,這個算法究竟是個什麼意思呢?我本身的理解是這樣的:

好比仍是前面那個變成垃圾的數組$a對應的zval,命名爲zval_a,  若是沒有執行unset, zval_a的refcount爲2,分別由$a和$a中的索引1指向這個zval。  用算法對這個數組中的全部元素(索引0和索引1)的zval的refcount進行減1操做,因爲索引1對應的就是zval_a,因此這個時候zval_a的refcount應該變成了1,這樣zval_a就不是一個垃圾。若是執行了unset操做,zval_a的refcount就是1,由zval_a中的索引1指向zval_a,用算法對數組中的全部元素(索引0和索引1)的zval的refcount進行減1操做,這樣zval_a的refcount就會變成0,因而就發現zval_a是一個垃圾了。 算法就這樣發現了頑固的垃圾數據。

舉了這個例子,讀者大概應該可以知道其中的端倪:

對於一個包含環形引用的數組,對數組中包含的每一個元素的zval進行減1操做,以後若是發現數組自身的zval的refcount變成了0,那麼能夠判斷這個數組是一個垃圾。

這個道理其實很簡單,假設數組a的refcount等於m, a中有n個元素又指向a,若是m等於n,那麼算法的結果是m減n,m-n=0,那麼a就是垃圾,若是m>n,那麼算法的結果m-n>0,因此a就不是垃圾了。

m=n表明什麼?  表明a的refcount都來自數組a自身包含的zval元素,表明a以外沒有任何變量指向它,表明用戶代碼空間中沒法再訪問到a所對應的zval,表明a是泄漏的內存,所以GC將a這個垃圾回收了。

在PHP中,GC默認是開啓的,你能夠經過ini文件中的 zend.enable_gc 項來開啓或則關閉GC。當GC開啓的時候,垃圾分析算法將在節點緩衝區(roots buffer)滿了以後啓動。緩衝區默承認以放10,000個節點,固然你也能夠經過修改Zend/zend_gc.c中的GC_ROOT_BUFFER_MAX_ENTRIES 來改變這個數值,須要從新編譯連接PHP。當GC關閉的時候,垃圾分析算法就不會運行,可是相關節點還會被放入節點緩衝區,這個時候若是緩衝區節點已經放滿,那麼新的節點就不會被記錄下來,這些沒有被記錄下來的節點就永遠也不會被垃圾分析算法分析。若是這些節點中有循環引用,那麼有可能產生內存泄漏。之因此在GC關閉的時候還要記錄這些節點,是由於簡單的記錄這些節點比在每次產生節點的時候判斷GC是否開啓更快,另外GC是能夠在腳本運行中開啓的,因此記錄下這些節點,在代碼運行的某個時候若是又開啓了GC,這些節點就能被分析算法分析。固然垃圾分析算法是一個比較耗時的操做。

在PHP代碼中咱們能夠經過gc_enable()和gc_disable()函數來開啓和關閉GC,也能夠經過調用gc_collect_cycles()在節點緩衝區未滿的狀況下強制執行垃圾分析算法。這樣用戶就能夠在程序的某些部分關閉或則開啓GC,也可強制進行垃圾分析算法。 

新的GC算法的性能

1. 防止泄漏節省內存

新的GC算法的目的就是爲了防止循環引用的變量引發的內存泄漏問題,在PHP中GC算法,當節點緩衝區滿了以後,垃圾分析算法會啓動,而且會釋放掉髮現的垃圾,從而回收內存,在PHP手冊上給了一段代碼和內存使用情況圖:

<?php
class Foo
{
    public $var = '3.1415962654';
}

$baseMemory = memory_get_usage();

for ( $i = 0; $i <= 100000; $i++ )
{
    $a = new Foo;
    $a->self = $a;
    if ( $i % 500 === 0 )
    {
        echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "/n";
    }
}
?>

這段代碼的循環體中,新建了一個對象變量,而且用對象的一個成員指向了本身,這樣就造成了一個循環引用,當進入下一次循環的時候,又一次給對象變量從新賦值,這樣會致使以前的對象變量內存泄漏,在這個例子裏面有兩個變量泄漏了,一個是對象自己,另一個是對象中的成員self,可是這兩個變量只有對象會做爲垃圾收集器的節點被放入緩衝區(由於從新賦值至關於對它進行了unset操做,知足前面的準則3)。在這裏咱們進行了100,000次循環,而GC在緩衝區中有10,000節點的時候會啓動垃圾分析算法,因此這裏一共會進行10次的垃圾分析算法。從圖中能夠清晰的看到,在5.3版本PHP中,每次GC的垃圾分析算法被觸發後,內存會有一個明顯的減小。而在5.2版本的PHP中,內存使用量會一直增長。

2. 運行效率影響

啓用了新的GC後,垃圾分析算法將是一個比較耗時的操做,手冊中給了一段測試代碼:

<?php
class Foo
{
    public $var = '3.1415962654';
}

for ( $i = 0; $i <= 1000000; $i++ )
{
    $a = new Foo;
    $a->self = $a;
}

echo memory_get_peak_usage(), "/n";
?>

而後分別在GC開啓和關閉的狀況下執行這段代碼:

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

最終在該機器上,第一次執行大概使用10.7秒,第二次執行大概使用11.4秒,性能大約下降7%,不過內存的使用量下降了98%,從931M下降到了10M。固然這並非一個比較科學的測試方法,可是也能說明必定的問題。這種代碼測試的是一種極端惡劣條件,實際代碼中,特別是在WEB的應用中,很難出現大量循環引用,GC的分析算法的啓動不會這麼頻繁,小規模的代碼中甚至不多有機會啓動GC分析算法。

總結:

當GC的垃圾分析算法執行的時候,PHP腳本的效率會受到必定的影響,可是小規模的代碼通常不會有這個機會運行這個算法。若是一旦腳本中GC分析算法開始運行了,那麼將花費少許的時間節省出來了大量的內存,是一件很是划算的事情。新的GC對一些長期運行的PHP腳本效果更好,好比PHP的DAEMON守護進程,或則PHP-GTK進程等等。

相關文章
相關標籤/搜索