PHP内核探索:新垃圾回收机制说明
在5.2及更早版本的PHP中,沒(méi)有專(zhuān)門(mén)的垃圾回收器GC(Garbage Collection),引擎在判斷一個(gè)變量空間是否能夠被釋放的時(shí)候是依據(jù)這個(gè)變量的zval的refcount的值,如果refcount為0,那么變量的空間可以被釋放,否則就不釋放,這是一種非常簡(jiǎn)單的GC實(shí)現(xiàn)。然而在這種簡(jiǎn)單的GC實(shí)現(xiàn)方案中,出現(xiàn)了意想不到的變量?jī)?nèi)存泄漏情況(Bug:http://bugs.php.net/bug.php?id=33595),引擎將無(wú)法回收這些內(nèi)存,于是在PHP5.3中出現(xiàn)了新的GC,新的GC有專(zhuān)門(mén)的機(jī)制負(fù)責(zé)清理垃圾數(shù)據(jù),防止內(nèi)存泄漏。本文將詳細(xì)的闡述PHP5.3中新的GC運(yùn)行機(jī)制。
目前很少有詳細(xì)的資料介紹新的GC,本文將是目前國(guó)內(nèi)最為詳細(xì)的從源碼角度介紹PHP5.3中GC原理的文章。其中關(guān)于垃圾產(chǎn)生以及算法簡(jiǎn)介部分由筆者根據(jù)手冊(cè)翻譯而來(lái),當(dāng)然其中融入了本人的一些看法。手冊(cè)中相關(guān)內(nèi)容:Garbage Collection
什么算垃圾
首先我們需要定義一下“垃圾”的概念,新的GC負(fù)責(zé)清理的垃圾是指變量的容器zval還存在,但是又沒(méi)有任何變量名指向此zval。因此GC判斷是否為垃圾的一個(gè)重要標(biāo)準(zhǔn)是有沒(méi)有變量名指向變量容器zval。
假設(shè)我們有一段PHP代碼,使用了一個(gè)臨時(shí)變量$tmp存儲(chǔ)了一個(gè)字符串,在處理完字符串之后,就不需要這個(gè)$tmp變量了,$tmp變量對(duì)于我們來(lái)說(shuō)可以算是一個(gè)“垃圾”了,但是對(duì)于GC來(lái)說(shuō),$tmp其實(shí)并不是一個(gè)垃圾,$tmp變量對(duì)我們沒(méi)有意義,但是這個(gè)變量實(shí)際還存在,$tmp符號(hào)依然指向它所對(duì)應(yīng)的zval,GC會(huì)認(rèn)為PHP代碼中可能還會(huì)使用到此變量,所以不會(huì)將其定義為垃圾。
那么如果我們?cè)赑HP代碼中使用完$tmp后,調(diào)用unset刪除這個(gè)變量,那么$tmp是不是就成為一個(gè)垃圾了呢。很可惜,GC仍然不認(rèn)為$tmp是一個(gè)垃圾,因?yàn)?tmp在unset之后,refcount減少1變成了0(這里假設(shè)沒(méi)有別的變量和$tmp指向相同的zval),這個(gè)時(shí)候GC會(huì)直接將$tmp對(duì)應(yīng)的zval的內(nèi)存空間釋放,$tmp和其對(duì)應(yīng)的zval就根本不存在了。此時(shí)的$tmp也不是新的GC所要對(duì)付的那種“垃圾”。那么新的GC究竟要對(duì)付什么樣的垃圾呢,下面我們將生產(chǎn)一個(gè)這樣的垃圾。 ?
頑固垃圾的產(chǎn)生過(guò)程
如果讀者已經(jīng)閱讀了變量?jī)?nèi)部存儲(chǔ)相關(guān)的內(nèi)容,想必對(duì)refcount和isref這些變量?jī)?nèi)部的信息有了一定的了解。這里我們將結(jié)合手冊(cè)中的一個(gè)例子來(lái)介紹垃圾的產(chǎn)生過(guò)程:
| 1 2 3 | <?php $a = "new string"; ?> |
在這么簡(jiǎn)單的一個(gè)代碼中,$a變量?jī)?nèi)部存儲(chǔ)信息為:a: (refcount=1, is_ref=0)='new string'
當(dāng)把$a賦值給另外一個(gè)變量的時(shí)候,$a對(duì)應(yīng)的zval的refcount會(huì)加1。
| 1 2 3 4 | <?php $a = "new string"; $b = $a; ?> |
此時(shí)$a和$b變量對(duì)應(yīng)的內(nèi)部存儲(chǔ)信息為 a,b: (refcount=2, is_ref=0)='new string'
當(dāng)我們用unset刪除$b變量的時(shí)候,$b對(duì)應(yīng)的zval的refcount會(huì)減少
| 1 2 3 4 5 | <?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' ?> |
對(duì)于普通的變量來(lái)說(shuō),這一切似乎很正常,但是在復(fù)合類(lèi)型變量(數(shù)組和對(duì)象)中,會(huì)發(fā)生比較有意思的事情:
| 1 2 3 | <?php $a = array('meaning'=> 'life','number' => 42); ?> |
a的內(nèi)部存儲(chǔ)信息為:
a: (refcount=1, is_ref=0)=array (
? 'meaning' => (refcount=1, is_ref=0)='life',
? 'number' => (refcount=1, is_ref=0)=4)
數(shù)組變量本身($a)在引擎內(nèi)部實(shí)際上是一個(gè)哈希表,這張表中有兩個(gè)zval項(xiàng) meaning和number,所以實(shí)際上那一行代碼中一共生成了3個(gè)zval,這3個(gè)zval都遵循變量的引用和計(jì)數(shù)原則,用圖來(lái)表示:
下面在$a中添加一個(gè)元素,并將現(xiàn)有的一個(gè)元素的值賦給新的元素:
| 1 2 3 4 | <?php $a = array('meaning'=> 'life','number' => 42); $a['life'] =$a['meaning']; ?> |
那么$a的內(nèi)部存儲(chǔ)為:
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元素之指向同一個(gè)zval的:
現(xiàn)在,如果我們?cè)囈幌?#xff0c;將數(shù)組的引用賦值給數(shù)組中的一個(gè)元素,有意思的事情就發(fā)生了:
| 1 2 3 4 | <?php $a = array('one'); $a[] = &$a; ?> |
這樣$a數(shù)組就有兩個(gè)元素,一個(gè)索引為0,值為字符one,另外一個(gè)索引為1,為$a自身的引用,內(nèi)部存儲(chǔ)如下:
a: (refcount=2, is_ref=1)=array (
? 0 => (refcount=1, is_ref=0)='one',
? 1 => (refcount=2, is_ref=1)=...
)
“...”表示1指向a自身,是一個(gè)環(huán)形引用:
這個(gè)時(shí)候我們對(duì)$a進(jìn)行unset,那么$a會(huì)從符號(hào)表中刪除,同時(shí)$a指向的zval的refcount減少
| 1 2 3 4 5 | <?php $a = array('one'); $a[] = &$a; unset($a); ?> |
那么問(wèn)題也就產(chǎn)生了,$a已經(jīng)不在符號(hào)表中了,用戶無(wú)法再訪問(wèn)此變量,但是$a之前指向的zval的refcount變?yōu)?而不是0,因此不能被回收,這樣產(chǎn)生了內(nèi)存泄露:
這樣,這么一個(gè)zval就成為了一個(gè)真是意義的垃圾了,新的GC要做的工作就是清理這種垃圾。
新的GC算法
為解決這種垃圾,產(chǎn)生了新的GC。
在PHP5.3版本中,使用了專(zhuān)門(mén)GC機(jī)制清理垃圾,在之前的版本中是沒(méi)有專(zhuān)門(mén)的GC,那么垃圾產(chǎn)生的時(shí)候,沒(méi)有辦法清理,內(nèi)存就白白浪費(fèi)掉了。在PHP5.3源代碼中多了以下文件:{PHPSRC}/Zend/zend_gc.h {PHPSRC}/Zend/zend_gc.c, 這里就是新的GC的實(shí)現(xiàn),我們先簡(jiǎn)單的介紹一下算法思路,然后再?gòu)脑创a的角度詳細(xì)介紹引擎中如何實(shí)現(xiàn)這個(gè)算法的。
在較新的PHP手冊(cè)中有簡(jiǎn)單的介紹新的GC使用的垃圾清理算法,這個(gè)算法名為 Concurrent Cycle Collection in Reference Counted Systems , 這里不詳細(xì)介紹此算法,根據(jù)手冊(cè)中的內(nèi)容來(lái)先簡(jiǎn)單的介紹一下思路:
首先我們有幾個(gè)基本的準(zhǔn)則:
如果一個(gè)zval的refcount增加,那么此zval還在使用,不屬于垃圾
如果一個(gè)zval的refcount減少到0, 那么zval可以被釋放掉,不屬于垃圾
如果一個(gè)zval的refcount減少之后大于0,那么此zval還不能被釋放,此zval可能成為一個(gè)垃圾
只有在準(zhǔn)則3下,GC才會(huì)把zval收集起來(lái),然后通過(guò)新的算法來(lái)判斷此zval是否為垃圾。那么如何判斷這么一個(gè)變量是否為真正的垃圾呢?
簡(jiǎn)單的說(shuō),就是對(duì)此zval中的每個(gè)元素進(jìn)行一次refcount減1操作,操作完成之后,如果zval的refcount=0,那么這個(gè)zval就是一個(gè)垃圾。這個(gè)原理咋看起來(lái)很簡(jiǎn)單,但是又不是那么容易理解,起初筆者也無(wú)法理解其含義,直到挖掘了源代碼之后才算是了解。如果你現(xiàn)在不理解沒(méi)有關(guān)系,后面會(huì)詳細(xì)介紹,這里先把這算法的幾個(gè)步驟描敘一下,首先引用手冊(cè)中的一張圖:
A:為了避免每次變量的refcount減少的時(shí)候都調(diào)用GC的算法進(jìn)行垃圾判斷,此算法會(huì)先把所有前面準(zhǔn)則3情況下的zval節(jié)點(diǎn)放入一個(gè)節(jié)點(diǎn)(root)緩沖區(qū)(root buffer),并且將這些zval節(jié)點(diǎn)標(biāo)記成紫色,同時(shí)算法必須確保每一個(gè)zval節(jié)點(diǎn)在緩沖區(qū)中之出現(xiàn)一次。當(dāng)緩沖區(qū)被節(jié)點(diǎn)塞滿的時(shí)候,GC才開(kāi)始開(kāi)始對(duì)緩沖區(qū)中的zval節(jié)點(diǎn)進(jìn)行垃圾判斷。
B:當(dāng)緩沖區(qū)滿了之后,算法以深度優(yōu)先對(duì)每一個(gè)節(jié)點(diǎn)所包含的zval進(jìn)行減1操作,為了確保不會(huì)對(duì)同一個(gè)zval的refcount重復(fù)執(zhí)行減1操作,一旦zval的refcount減1之后會(huì)將zval標(biāo)記成灰色。需要強(qiáng)調(diào)的是,這個(gè)步驟中,起初節(jié)點(diǎn)zval本身不做減1操作,但是如果節(jié)點(diǎn)zval中包含的zval又指向了節(jié)點(diǎn)zval(環(huán)形引用),那么這個(gè)時(shí)候需要對(duì)節(jié)點(diǎn)zval進(jìn)行減1操作。
C:算法再次以深度優(yōu)先判斷每一個(gè)節(jié)點(diǎn)包含的zval的值,如果zval的refcount等于0,那么將其標(biāo)記成白色(代表垃圾),如果zval的refcount大于0,那么將對(duì)此zval以及其包含的zval進(jìn)行refcount加1操作,這個(gè)是對(duì)非垃圾的還原操作,同時(shí)將這些zval的顏色變成黑色(zval的默認(rèn)顏色屬性)。
D:遍歷zval節(jié)點(diǎn),將C中標(biāo)記成白色的節(jié)點(diǎn)zval釋放掉。
這ABCD四個(gè)過(guò)程是手冊(cè)中對(duì)這個(gè)算法的介紹,這還不是那么容易理解其中的原理,這個(gè)算法到底是個(gè)什么意思呢?我自己的理解是這樣的:
比如還是前面那個(gè)變成垃圾的數(shù)組$a對(duì)應(yīng)的zval,命名為zval_a, ?如果沒(méi)有執(zhí)行unset, zval_a的refcount為2,分別由$a和$a中的索引1指向這個(gè)zval。 ?用算法對(duì)這個(gè)數(shù)組中的所有元素(索引0和索引1)的zval的refcount進(jìn)行減1操作,由于索引1對(duì)應(yīng)的就是zval_a,所以這個(gè)時(shí)候zval_a的refcount應(yīng)該變成了1,這樣zval_a就不是一個(gè)垃圾。如果執(zhí)行了unset操作,zval_a的refcount就是1,由zval_a中的索引1指向zval_a,用算法對(duì)數(shù)組中的所有元素(索引0和索引1)的zval的refcount進(jìn)行減1操作,這樣zval_a的refcount就會(huì)變成0,于是就發(fā)現(xiàn)zval_a是一個(gè)垃圾了。 算法就這樣發(fā)現(xiàn)了頑固的垃圾數(shù)據(jù)。
舉了這個(gè)例子,讀者大概應(yīng)該能夠知道其中的端倪:
對(duì)于一個(gè)包含環(huán)形引用的數(shù)組,對(duì)數(shù)組中包含的每個(gè)元素的zval進(jìn)行減1操作,之后如果發(fā)現(xiàn)數(shù)組自身的zval的refcount變成了0,那么可以判斷這個(gè)數(shù)組是一個(gè)垃圾。
這個(gè)道理其實(shí)很簡(jiǎn)單,假設(shè)數(shù)組a的refcount等于m, a中有n個(gè)元素又指向a,如果m等于n,那么算法的結(jié)果是m減n,m-n=0,那么a就是垃圾,如果m>n,那么算法的結(jié)果m-n>0,所以a就不是垃圾了。
m=n代表什么? ?代表a的refcount都來(lái)自數(shù)組a自身包含的zval元素,代表a之外沒(méi)有任何變量指向它,代表用戶代碼空間中無(wú)法再訪問(wèn)到a所對(duì)應(yīng)的zval,代表a是泄漏的內(nèi)存,因此GC將a這個(gè)垃圾回收了。
在PHP中,GC默認(rèn)是開(kāi)啟的,你可以通過(guò)ini文件中的 zend.enable_gc 項(xiàng)來(lái)開(kāi)啟或則關(guān)閉GC。當(dāng)GC開(kāi)啟的時(shí)候,垃圾分析算法將在節(jié)點(diǎn)緩沖區(qū)(roots buffer)滿了之后啟動(dòng)。緩沖區(qū)默認(rèn)可以放10,000個(gè)節(jié)點(diǎn),當(dāng)然你也可以通過(guò)修改Zend/zend_gc.c中的GC_ROOT_BUFFER_MAX_ENTRIES 來(lái)改變這個(gè)數(shù)值,需要重新編譯鏈接PHP。當(dāng)GC關(guān)閉的時(shí)候,垃圾分析算法就不會(huì)運(yùn)行,但是相關(guān)節(jié)點(diǎn)還會(huì)被放入節(jié)點(diǎn)緩沖區(qū),這個(gè)時(shí)候如果緩沖區(qū)節(jié)點(diǎn)已經(jīng)放滿,那么新的節(jié)點(diǎn)就不會(huì)被記錄下來(lái),這些沒(méi)有被記錄下來(lái)的節(jié)點(diǎn)就永遠(yuǎn)也不會(huì)被垃圾分析算法分析。如果這些節(jié)點(diǎn)中有循環(huán)引用,那么有可能產(chǎn)生內(nèi)存泄漏。之所以在GC關(guān)閉的時(shí)候還要記錄這些節(jié)點(diǎn),是因?yàn)楹?jiǎn)單的記錄這些節(jié)點(diǎn)比在每次產(chǎn)生節(jié)點(diǎn)的時(shí)候判斷GC是否開(kāi)啟更快,另外GC是可以在腳本運(yùn)行中開(kāi)啟的,所以記錄下這些節(jié)點(diǎn),在代碼運(yùn)行的某個(gè)時(shí)候如果又開(kāi)啟了GC,這些節(jié)點(diǎn)就能被分析算法分析。當(dāng)然垃圾分析算法是一個(gè)比較耗時(shí)的操作。
在PHP代碼中我們可以通過(guò)gc_enable()和gc_disable()函數(shù)來(lái)開(kāi)啟和關(guān)閉GC,也可以通過(guò)調(diào)用gc_collect_cycles()在節(jié)點(diǎn)緩沖區(qū)未滿的情況下強(qiáng)制執(zhí)行垃圾分析算法。這樣用戶就可以在程序的某些部分關(guān)閉或則開(kāi)啟GC,也可強(qiáng)制進(jìn)行垃圾分析算法。
新的GC算法的性能
1. 防止泄漏節(jié)省內(nèi)存
新的GC算法的目的就是為了防止循環(huán)引用的變量引起的內(nèi)存泄漏問(wèn)題,在PHP中GC算法,當(dāng)節(jié)點(diǎn)緩沖區(qū)滿了之后,垃圾分析算法會(huì)啟動(dòng),并且會(huì)釋放掉發(fā)現(xiàn)的垃圾,從而回收內(nèi)存,在PHP手冊(cè)上給了一段代碼和內(nèi)存使用狀況圖:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <?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 ) ????{ ????????echosprintf( '%8d: ',$i ), memory_get_usage() -$baseMemory, "/n"; ????} } ?> |
這段代碼的循環(huán)體中,新建了一個(gè)對(duì)象變量,并且用對(duì)象的一個(gè)成員指向了自己,這樣就形成了一個(gè)循環(huán)引用,當(dāng)進(jìn)入下一次循環(huán)的時(shí)候,又一次給對(duì)象變量重新賦值,這樣會(huì)導(dǎo)致之前的對(duì)象變量?jī)?nèi)存泄漏,在這個(gè)例子里面有兩個(gè)變量泄漏了,一個(gè)是對(duì)象本身,另外一個(gè)是對(duì)象中的成員self,但是這兩個(gè)變量只有對(duì)象會(huì)作為垃圾收集器的節(jié)點(diǎn)被放入緩沖區(qū)(因?yàn)橹匦沦x值相當(dāng)于對(duì)它進(jìn)行了unset操作,滿足前面的準(zhǔn)則3)。在這里我們進(jìn)行了100,000次循環(huán),而GC在緩沖區(qū)中有10,000節(jié)點(diǎn)的時(shí)候會(huì)啟動(dòng)垃圾分析算法,所以這里一共會(huì)進(jìn)行10次的垃圾分析算法。從圖中可以清晰的看到,在5.3版本PHP中,每次GC的垃圾分析算法被觸發(fā)后,內(nèi)存會(huì)有一個(gè)明顯的減少。而在5.2版本的PHP中,內(nèi)存使用量會(huì)一直增加。
2. 運(yùn)行效率影響
啟用了新的GC后,垃圾分析算法將是一個(gè)比較耗時(shí)的操作,手冊(cè)中給了一段測(cè)試代碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 | <?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開(kāi)啟和關(guān)閉的情況下執(zhí)行這段代碼:
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
最終在該機(jī)器上,第一次執(zhí)行大概使用10.7秒,第二次執(zhí)行大概使用11.4秒,性能大約降低7%,不過(guò)內(nèi)存的使用量降低了98%,從931M降低到了10M。當(dāng)然這并不是一個(gè)比較科學(xué)的測(cè)試方法,但是也能說(shuō)明一定的問(wèn)題。這種代碼測(cè)試的是一種極端惡劣條件,實(shí)際代碼中,特別是在WEB的應(yīng)用中,很難出現(xiàn)大量循環(huán)引用,GC的分析算法的啟動(dòng)不會(huì)這么頻繁,小規(guī)模的代碼中甚至很少有機(jī)會(huì)啟動(dòng)GC分析算法。
總結(jié):
當(dāng)GC的垃圾分析算法執(zhí)行的時(shí)候,PHP腳本的效率會(huì)受到一定的影響,但是小規(guī)模的代碼一般不會(huì)有這個(gè)機(jī)會(huì)運(yùn)行這個(gè)算法。如果一旦腳本中GC分析算法開(kāi)始運(yùn)行了,那么將花費(fèi)少量的時(shí)間節(jié)省出來(lái)了大量的內(nèi)存,是一件非常劃算的事情。新的GC對(duì)一些長(zhǎng)期運(yùn)行的PHP腳本效果更好,比如PHP的DAEMON守護(hù)進(jìn)程,或則PHP-GTK進(jìn)程等等。
來(lái)源:http://blog.csdn.net/niluchen/article/details/9468365
與50位技術(shù)專(zhuān)家面對(duì)面20年技術(shù)見(jiàn)證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的PHP内核探索:新垃圾回收机制说明的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 台球桌(说一说台球桌的简介)
- 下一篇: PHP生成日历(实例详解)