概述
在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運行機制。
目前很少有詳細的資料介紹新的GC,本文將是目前國內最為詳細的從源碼角度介紹PHP5.3中GC原理的文章。其中關於垃圾產生以及算法簡介部分由筆者根據手冊翻譯而來,當然其中融入了本人的一些看法。手冊中相關內容:Garbage Collection
在介紹這個新的GC之前,讀者必須先了解PHP中變量的內部存儲相關知識,請先閱讀 變量的內部存儲:引用和計數
什么算垃圾
首先我們需要定義一下“垃圾”的概念,新的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'
當把$a賦值給另外一個變量的時候,$a對應的zval的refcount會加1
<?php
$a = "new string";
$b = $a;
?>
此時$a和$b變量對應的內部存儲信息為
a,b: (refcount=2, is_ref=0)='new string'
當我們用unset刪除$b變量的時候,$b對應的zval的refcount會減少1
<?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
在PHP5.3版本中,使用了專門GC機制清理垃圾,在之前的版本中是沒有專門的GC,那么垃圾產生的時候,沒有辦法清理,內存就白白浪費掉了。在PHP5.3源代碼中多了以下文件:{PHPSRC}/Zend/zend_gc.h {PHPSRC}/Zend/zend_gc.c, 這里就是新的GC的實現,我們先簡單的介紹一下算法思路,然后再從源碼的角度詳細介紹引擎中如何實現這個算法的。
新的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的算法
在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進程等等。
引擎內部GC的實現
前面已經介紹了新的GC的基本原理以及性能相關的內容,其中一些都是在手冊中有簡單介紹了,那么這里我們將從源代碼的角度來分析一下PHP如何實現新的GC。
1.zval的變化
在文件Zend/zend_gc.h中,重新定義了分配一個zval結構的宏:
[cpp] view plain copy
-
#undef ALLOC_ZVAL
-
#define ALLOC_ZVAL(z) /
-
do { /
-
(z) = (zval*)emalloc(sizeof(zval_gc_info)); /
-
GC_ZVAL_INIT(z); /
-
} while (0)
ALLOC_ZVAL的原始定義是在Zend/zend_alloc.h中,原始的定義只是分配一個zval結構的內存空間,然后在新的GC使用后,分配一個zval空間實際上是分配了一個zval_gc_info結構的空間,下面看看zval_gc_info結構定義:
[cpp] view plain copy
-
typedef struct _zval_gc_info {
-
zval z;
-
union {
-
gc_root_buffer *buffered;
-
struct _zval_gc_info *next;
-
} u;
-
} zval_gc_info;
zval_gc_info這個結構的第一個成員就是一個zval結構,第二個成員是一個聯合體u,是一個指向gc_root_buffer的指針和一個指向_zval_gc_info的指針。 第一個成員為zval結構,這就保證了對zval_gc_info類型指針做類型轉換后和zval等價。在ALLOC_ZVAL宏中,分配了一個zval_gc_info的空間后,是將空間的指針轉換成了(zval *)。這樣就相當於分配了一個zval的空間。然后GC_ZVAL_INIT宏會把zval_gc_info中的成員u的buffered字段設置成NULL:
[cpp] view plain copy
-
#define GC_ZVAL_INIT(z) /
-
((zval_gc_info*)(z))->u.buffered = NULL
這個u.buffered指針就是用來表示這個zval對應的節點信息指針。
新的GC會為所有的zval分配一個空間存放節點信息指針,只有當zval被GC放入節點緩沖區的時候,節點信息指針才會被指向一個節點信息結構,否則節點信息指針一直是NULL。
具體方式是通過分配一個zval_gc_info結構來實現,這個結構包含了zval和節點信息指針buffered。
2.節點信息
zval的節點信息指針buffered指向一個gc_root_buffer類型,這個類型的定義如下:
[cpp] view plain copy
-
typedef struct _gc_root_buffer {
-
struct _gc_root_buffer *prev; /* double-linked list */
-
struct _gc_root_buffer *next;
-
zend_object_handle handle; /* must be 0 for zval */
-
union {
-
zval *pz;
-
zend_object_handlers *handlers;
-
} u;
-
} gc_root_buffer;
這是一個雙鏈表的節點結構類型,prev和next用來指向前一個節點和后一個節點,handel是和對象相關的,對象類型的變量比較特殊,我們這里不討論,u是一個聯合體,u.pz用來指向這個節點所對應的zval結構。 這樣每一個zval結構和zval對應的節點信息互相被關聯在一起了:
通過一個zval指針pz找到節點指針: pr = ((zval_gc_info *)pz)->u.buffered
通過一個節點指針pr找到zval指針: pz = pr->u.pz
3.為zval設置節點信息以及節點顏色信息
這里GC應用了一些小技巧,先看看下面相關的宏:
[cpp] view plain copy
-
#define GC_COLOR 0x03
-
-
#define GC_BLACK 0x00
-
#define GC_WHITE 0x01
-
#define GC_GREY 0x02
-
#define GC_PURPLE 0x03
-
-
#define GC_ADDRESS(v) /
-
((gc_root_buffer*)(((zend_uintptr_t)(v)) & ~GC_COLOR))
-
#define GC_SET_ADDRESS(v, a) /
-
(v) = ((gc_root_buffer*)((((zend_uintptr_t)(v)) & GC_COLOR) | ((zend_uintptr_t)(a))))
-
#define GC_GET_COLOR(v) /
-
(((zend_uintptr_t)(v)) & GC_COLOR)
-
#define GC_SET_COLOR(v, c) /
-
(v) = ((gc_root_buffer*)((((zend_uintptr_t)(v)) & ~GC_COLOR) | (c)))
-
#define GC_SET_BLACK(v) /
-
(v) = ((gc_root_buffer*)(((zend_uintptr_t)(v)) & ~GC_COLOR))
-
#define GC_SET_PURPLE(v) /
-
(v) = ((gc_root_buffer*)(((zend_uintptr_t)(v)) | GC_PURPLE))
-
-
#define GC_ZVAL_INIT(z) /
-
((zval_gc_info*)(z))->u.buffered = NULL
-
#define GC_ZVAL_ADDRESS(v) /
-
GC_ADDRESS(((zval_gc_info*)(v))->u.buffered)
-
#define GC_ZVAL_SET_ADDRESS(v, a) /
-
GC_SET_ADDRESS(((zval_gc_info*)(v))->u.buffered, (a))
-
#define GC_ZVAL_GET_COLOR(v) /
-
GC_GET_COLOR(((zval_gc_info*)(v))->u.buffered)
-
#define GC_ZVAL_SET_COLOR(v, c) /
-
GC_SET_COLOR(((zval_gc_info*)(v))->u.buffered, (c))
-
#define GC_ZVAL_SET_BLACK(v) /
-
GC_SET_BLACK(((zval_gc_info*)(v))->u.buffered)
-
#define GC_ZVAL_SET_PURPLE(v) /
-
GC_SET_PURPLE(((zval_gc_info*)(v))->u.buffered)
其中宏GC_ZVAL_SET_ADDRESS(v, a)是為v這個zval設置節點信息的指針a,這個宏先得到v中的節點信息指針字段u.buffered,然后調用GC_ADDRESS(v,a)宏,將u.buffered字段設置成指針a。
GC_ADDRESS(v, a)宏的功能是將地址a賦給v,但是它的實現很奇怪:
(v) = ((gc_root_buffer*)((((zend_uintptr_t)(v)) & GC_COLOR) | ((zend_uintptr_t)(a))))
為什么需要這么一個復雜的過程,而且設置指針值為何還要牽扯到GC_COLOR顏色這個宏?
這里就得先說說節點的顏色信息保存方式。
在前面GC的算法簡介中,提到了需要為節點上色,而實際在我們節點結構gc_root_buffer中並沒有哪一個字段用來標識節點的顏色,這里GC運用了一個小的技巧:利用節點指針的低兩位來標識顏色屬性。可能讀者會有疑問,用指針中的位來保存顏色屬性,那么設置顏色后,指針不就變化了嗎,那么還能查找到指針對應的結構嗎? 這個還真能查到! 為什么? 這個和malloc分配的內存地址屬性有一定的關系,glib的malloc分配的內存地址都會有一定的對齊,這個對齊值為2 * SIZE_SZ,在不同位的機器上這個值是不一樣的,但是可以確保的是分配出來的指針的最低兩位肯定是0,然后看看顏色相關的宏,GC_COLOR為0x03, 3只需要兩個二進制位就能夠保存,所以拿指針的最低兩位來保存顏色值是沒有任何問題的,但是在使用指針的時候一定要先把指針最低的兩位還原成0,否則指針指向的值是錯誤的。
這樣我們就能理解為什么GC_ADDRESS需要這么復雜了。因為v中的低2位保存了v的顏色信息,如果直接把a賦給v會覆蓋掉顏色信息,通過((zend_uintptr_t)(v)) & GC_COLOR可以保留低兩位的顏色信息,同時其它的位都變成了0,將這個結果同a進行"|"操作,就能將a的賦給v,同時保留了v的顏色信息。
知道了顏色信息的存儲方式,那么就應該很容易理解如何設置和獲取顏色信息,這里就不多介紹了。
4.節點緩沖區
GC會將收集到的節點存放到一個緩沖區中,緩沖區滿的時候就開始進行垃圾分析算法。這個緩沖區實際上放在一個全局的結構中:
[cpp] view plain copy
-
typedef struct _zend_gc_globals {
-
zend_bool gc_enabled;
-
zend_bool gc_active;
-
-
gc_root_buffer *buf; /* preallocated arrays of buffers */
-
gc_root_buffer roots; /* list of possible roots of cycles */
-
gc_root_buffer *unused; /* list of unused buffers */
-
gc_root_buffer *first_unused; /* pointer to first unused buffer */
-
gc_root_buffer *last_unused; /* pointer to last unused buffer */
-
-
zval_gc_info *zval_to_free; /* temporaryt list of zvals to free */
-
zval_gc_info *free_list;
-
zval_gc_info *next_to_free;
-
-
zend_uint gc_runs;
-
zend_uint collected;
-
-
#if GC_BENCH
-
zend_uint root_buf_length;
-
zend_uint root_buf_peak;
-
zend_uint zval_possible_root;
-
zend_uint zobj_possible_root;
-
zend_uint zval_buffered;
-
zend_uint zobj_buffered;
-
zend_uint zval_remove_from_buffer;
-
zend_uint zobj_remove_from_buffer;
-
zend_uint zval_marked_grey;
-
zend_uint zobj_marked_grey;
-
#endif
-
-
} zend_gc_globals;
用宏GC_G(v)可以訪問結構中的v字段。
簡單的介紹這個結構中幾個重要的字段的含義:
zend_bool gc_enabled:
是否開啟GC
zend_bool gc_active:
GC是否正在進行垃圾分析
gc_root_buffer *buf:
節點緩沖區指針,在GC初始化的時候,會分配10,000個gc_root_buffer結構的空間,buf為第1個節點的地址
gc_root_buffer roots;
GC每次開始垃圾分析算法的時候,都是從這個節點開始進行(注意不是直接在緩沖區中按順序來分析節點,緩沖區值是存放節點信息內容,roots是分析的節點入口,是一個雙鏈表的入口)
其他節點和垃圾分析過程中的一些臨時數據有關,這里暫不介紹。
5.GC的初始化
[cpp] view plain copy
-
ZEND_API void gc_init(TSRMLS_D)
-
{
-
if (GC_G(buf) == NULL && GC_G(gc_enabled)) {
-
GC_G(buf) = (gc_root_buffer*) malloc(sizeof(gc_root_buffer) * GC_ROOT_BUFFER_MAX_ENTRIES);
-
GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES];
-
gc_reset(TSRMLS_C);
-
}
-
}
首先在初始化之前會有一個全局變量
extern ZEND_API zend_gc_globals gc_globals;
在整個GC運行期間都依賴這個全局變量結構。
初始化是調用的gc_init函數,如果緩沖區指針字段為空並且GC開啟,那么就分配緩沖區,然后調用gc_reset初始化全局結構gc_globals中的相關字段。
6.節點放入緩沖區的時機
那么現在就是一個比較關鍵的一步了,GC何時為zval設置節點信息,並將節點信息設置放入緩沖區等待分析處理。從前面介紹的GC算法的原理中,准則3:“如果一個zval的refcount減少之后大於0,那么此zval還不能被釋放,此zval可能成為一個垃圾”。我們大概可以知道當一個zval的refcount減少的時候,GC有可能為zval分配節點並放入緩沖區。那么在什么情況下zval的refcount會減少。 在我們調用unset的時候,會從當前符號的哈希表中刪除變量名對應的項,並對該項調用一個析構函數,所以這個refcount減少的操作發生在這個析構函數中。通過建立變量符號哈希表的代碼段可以知道這個析構函數是什么。這個析構函數最終的實現在Zend/zend_execute_API.c中:
[cpp] view plain copy
-
ZEND_API void _zval_ptr_dtor(zval **zval_ptr ZEND_FILE_LINE_DC) /* {{{ */
-
{
-
#if DEBUG_ZEND>=2
-
printf("Reducing refcount for %x (%x): %d->%d/n", *zval_ptr, zval_ptr, Z_REFCOUNT_PP(zval_ptr), Z_REFCOUNT_PP(zval_ptr) – 1);
-
#endif
-
Z_DELREF_PP(zval_ptr);
-
if (Z_REFCOUNT_PP(zval_ptr) == 0) {
-
TSRMLS_FETCH();
-
-
if (*zval_ptr != &EG(uninitialized_zval)) {
-
GC_REMOVE_ZVAL_FROM_BUFFER(*zval_ptr);
-
zval_dtor(*zval_ptr);
-
efree_rel(*zval_ptr);
-
}
-
} else {
-
TSRMLS_FETCH();
-
-
if (Z_REFCOUNT_PP(zval_ptr) == 1) {
-
Z_UNSET_ISREF_PP(zval_ptr);
-
}
-
-
GC_ZVAL_CHECK_POSSIBLE_ROOT(*zval_ptr);
-
}
-
}
這個函數中:
Z_DELREF_PP(zval_ptr) :對zval的refcount減1,減1之后
1.如果zval的refcount等於0,根據前面的准則2,這個變量的空間可以直接被釋放掉,在釋放之前需要注意,有可能這個變量在之前已經被放入了節點緩沖區,所以需要調用GC_REMOVE_ZVAL_FROM_BUFFER(*zval_ptr)從節點緩沖區中刪除相關節點信息,然后調用zval_dtor和efree_rel釋放掉變量zval中變量占用的空間和zval結構自身的空間。
2.如果zval的refcount等於1,根據前面的准則3,這個變量有可能會成為一個垃圾,於是調用GC_ZVAL_CHECK_POSSIBLE_ROOT(*zval_ptr)為其設置節點信息並放入緩沖區
因此,最終是通過GC_ZVAL_CHECK_POSSIBLE_ROOT宏來產生節點並放入緩沖等待處理,相關的宏和函數代碼為:
[cpp] view plain copy
-
#define GC_ZVAL_CHECK_POSSIBLE_ROOT(z) /
-
gc_zval_check_possible_root((z) TSRMLS_CC)
-
-
static zend_always_inline void gc_zval_check_possible_root(zval *z TSRMLS_DC)
-
{
-
if (z->type == IS_ARRAY || z->type == IS_OBJECT) {
-
gc_zval_possible_root(z TSRMLS_CC);
-
}
-
}
-
-
ZEND_API void gc_zval_possible_root(zval *zv TSRMLS_DC)
-
{
-
if (UNEXPECTED(GC_G(free_list) != NULL &&
-
GC_ZVAL_ADDRESS(zv) != NULL &&
-
GC_ZVAL_GET_COLOR(zv) == GC_BLACK) &&
-
(GC_ZVAL_ADDRESS(zv) < GC_G(buf) ||
-
GC_ZVAL_ADDRESS(zv) >= GC_G(last_unused))) {
-
/* The given zval is a garbage that is going to be deleted by
-
* currently running GC */
-
return;
-
}
-
-
if (zv->type == IS_OBJECT) {
-
GC_ZOBJ_CHECK_POSSIBLE_ROOT(zv);
-
return;
-
}
-
-
GC_BENCH_INC(zval_possible_root);
-
-
if (GC_ZVAL_GET_COLOR(zv) != GC_PURPLE) {
-
GC_ZVAL_SET_PURPLE(zv);
-
-
if (!GC_ZVAL_ADDRESS(zv)) {
-
gc_root_buffer *newRoot = GC_G(unused);
-
-
if (newRoot) {
-
GC_G(unused) = newRoot->prev;
-
} else if (GC_G(first_unused) != GC_G(last_unused)) {
-
newRoot = GC_G(first_unused);
-
GC_G(first_unused)++;
-
} else {
-
if (!GC_G(gc_enabled)) {
-
GC_ZVAL_SET_BLACK(zv);
-
return;
-
}
-
zv->refcount__gc++;
-
gc_collect_cycles(TSRMLS_C);
-
zv->refcount__gc–;
-
newRoot = GC_G(unused);
-
if (!newRoot) {
-
return;
-
}
-
GC_ZVAL_SET_PURPLE(zv);
-
GC_G(unused) = newRoot->prev;
-
}
-
-
newRoot->next = GC_G(roots).next;
-
newRoot->prev = &GC_G(roots);
-
GC_G(roots).next->prev = newRoot;
-
GC_G(roots).next = newRoot;
-
-
GC_ZVAL_SET_ADDRESS(zv, newRoot);
-
-
newRoot->handle = 0;
-
newRoot->u.pz = zv;
-
-
GC_BENCH_INC(zval_buffered);
-
GC_BENCH_INC(root_buf_length);
-
GC_BENCH_PEAK(root_buf_peak, root_buf_length);
-
}
-
}
-
}
內聯函數gc_zval_check_possible_root會先判斷zval的類型,如果是數組或則對象類型才有可能給zval分配節點信息並放入緩沖區。只有這兩種類型才可能產生環形引用。雖然GC直接處理對象是數組和對象類型,但是在這些數組和對象中包含的任何類型變量都在GC的職責范圍之內,這個內聯函數最終掉用的是gc_zval_possible_root函數,下面重點分析此函數中的主要流程:
1:
if (UNEXPECTED(GC_G(free_list) != NULL &&
GC_ZVAL_ADDRESS(zv) != NULL &&
GC_ZVAL_GET_COLOR(zv) == GC_BLACK) &&
(GC_ZVAL_ADDRESS(zv) < GC_G(buf) ||
GC_ZVAL_ADDRESS(zv) >= GC_G(last_unused))) {
/* The given zval is a garbage that is going to be deleted by
* currently running GC */
return;
}
首先檢查zval節點信息是否已經放入到節點緩沖區,如果已經放入到節點緩沖區,則直接返回,這樣保證節點緩沖區中的每個zval節點只出現一次。
2:
if (zv->type == IS_OBJECT) {
GC_ZOBJ_CHECK_POSSIBLE_ROOT(zv);
return;
}
如果zval是對象類型,則走對象類型相關的流程,本文只以數組類型為例講解,所以這個流程不闡述,讀者可以舉一反三。
3:
if (GC_ZVAL_GET_COLOR(zv) != GC_PURPLE) {
GC_ZVAL_SET_PURPLE(zv);
…
}
如果zval沒有被標記為紫色,就將其標記為紫色,表示zval被放入到節點緩沖,否則不做后面的操作。
4:
如果zval的節點信息指針為空,則需要為zval分配一個gc_root_buffer節點信息。這之后會有一些判斷機制,如果發現節點緩沖區已經滿了說明需要啟動垃圾分析流程了,垃圾分析流程在函數gc_collect_cycles(TSRMLS_C); 如果緩沖區沒有滿,則不會進入垃圾分析流程,為zval分配的節點信息會被加入到GC_G(roots)為入口的雙鏈表中。
從這個函數我們發現了垃圾分析算法是當發現緩沖區滿的時候就立即觸發,垃圾分析跟代碼執行流是同步過程,也就是只有垃圾分析結束之后,代碼才會繼續執行。所以在我們的PHP代碼中,如果某個unset正好使GC的節點緩沖區滿,觸發了垃圾分析流程,那么這個unset耗費的時間將比一般的unset多很多。
gc_collect_cycles函數是真正的垃圾分析流程,這個函數定義為:
[cpp] view plain copy
-
ZEND_API int gc_collect_cycles(TSRMLS_D)
-
{
-
int count = 0;
-
-
if (GC_G(roots).next != &GC_G(roots)) {
-
zval_gc_info *p, *q, *orig_free_list, *orig_next_to_free;
-
-
if (GC_G(gc_active)) {
-
return 0;
-
}
-
GC_G(gc_runs)++;
-
GC_G(zval_to_free) = FREE_LIST_END;
-
GC_G(gc_active) = 1;
-
gc_mark_roots(TSRMLS_C);
-
gc_scan_roots(TSRMLS_C);
-
gc_collect_roots(TSRMLS_C);
-
-
orig_free_list = GC_G(free_list);
-
orig_next_to_free = GC_G(next_to_free);
-
p = GC_G(free_list) = GC_G(zval_to_free);
-
GC_G(zval_to_free) = NULL;
-
GC_G(gc_active) = 0;
-
-
/* First call destructors */
-
while (p != FREE_LIST_END) {
-
if (Z_TYPE(p->z) == IS_OBJECT) {
-
if (EG(objects_store).object_buckets &&
-
EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].valid &&
-
EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount <= 0 &&
-
EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.dtor &&
-
!EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].destructor_called) {
-
-
EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].destructor_called = 1;
-
EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount++;
-
EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.dtor(EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.object, Z_OBJ_HANDLE(p->z) TSRMLS_CC);
-
EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount–;
-
}
-
}
-
count++;
-
p = p->u.next;
-
}
-
-
/* Destroy zvals */
-
p = GC_G(free_list);
-
while (p != FREE_LIST_END) {
-
GC_G(next_to_free) = p->u.next;
-
if (Z_TYPE(p->z) == IS_OBJECT) {
-
if (EG(objects_store).object_buckets &&
-
EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].valid &&
-
EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount <= 0) {
-
EG(objects_store).object_buckets[Z_OBJ_HANDLE(p->z)].bucket.obj.refcount = 1;
-
Z_TYPE(p->z) = IS_NULL;
-
zend_objects_store_del_ref_by_handle_ex(Z_OBJ_HANDLE(p->z), Z_OBJ_HT(p->z) TSRMLS_CC);
-
}
-
} else if (Z_TYPE(p->z) == IS_ARRAY) {
-
Z_TYPE(p->z) = IS_NULL;
-
zend_hash_destroy(Z_ARRVAL(p->z));
-
FREE_HASHTABLE(Z_ARRVAL(p->z));
-
} else {
-
zval_dtor(&p->z);
-
Z_TYPE(p->z) = IS_NULL;
-
}
-
p = GC_G(next_to_free);
-
}
-
-
/* Free zvals */
-
p = GC_G(free_list);
-
while (p != FREE_LIST_END) {
-
q = p->u.next;
-
FREE_ZVAL_EX(&p->z);
-
p = q;
-
}
-
GC_G(collected) += count;
-
GC_G(free_list) = orig_free_list;
-
GC_G(next_to_free) = orig_next_to_free;
-
}
-
-
return count;
-
}
這里只簡單的介紹其中關鍵的流程:
1.gc_mark_roots()
這個函數對節點信息的鏈表進行一次深度優先遍歷,將其中的zval的refcount減1,為了避免對同一個zval重復減操作,在操作之后將zval標記成灰色。(對節點自身的zval可以重復減操作,這個是此算法的基礎)
2.gc_scan_roots()
這個函數對節點信息的鏈表再次進行深度優先遍歷,如果發現zval的refcount大於等於1,則對該zval和其包含的zval的refcount加1操作,這個是對非垃圾的一個信息還原,然后將這些zval顏色屬性去掉(設置成black)。如果發現zval的refcount等於0,則就標記成白色,這些是后面將要清理掉的垃圾。
3.gc_collect_roots()
遍歷節點信息鏈表,將前面一個步驟中標記為白色的節點信息放到GC_G(zval_to_free)為入口的鏈表中,這個鏈表用來存放將要釋放的垃圾。 然后釋放掉全部的節點信息,緩沖區被清空。分析結束后將重新收集節點信息。
4.釋放步驟3中收集到垃圾數據。