垃圾的產(chǎn)生
之前的文章已經(jīng)介紹過(guò)PHP的引用計(jì)數(shù)機(jī)制-PHP內(nèi)核探索之變量-理解引用嚣镜,當(dāng)變量賦值、傳遞時(shí)并不會(huì)直接硬拷貝豆同,而是增加value的引用數(shù),unset含鳞、return等釋放變量時(shí)再減掉引用數(shù)影锈,減掉后如果發(fā)現(xiàn)refcount變?yōu)?則直接釋放value,這是變量的基本GC(Garbage Collection)過(guò)程。
但是在循環(huán)引用中鸭廷,是無(wú)法通過(guò)這一機(jī)制回收變量的枣抱。即當(dāng)數(shù)組或?qū)ο髢?nèi)部子元素引用其父元素,而此時(shí)如果發(fā)生了刪除其父元素的情況辆床,此變量容器并不會(huì)被刪除佳晶,因?yàn)閿?shù)組的引用計(jì)數(shù)中就有一個(gè)來(lái)自自身成員,試圖釋放數(shù)組時(shí)因?yàn)槠鋜efcount仍然大于0而得不到釋放讼载,而實(shí)際上已經(jīng)沒(méi)有任何外部引用了轿秧,所以無(wú)法被清除,因此會(huì)發(fā)生內(nèi)存泄漏咨堤。
下面看一個(gè)數(shù)組循環(huán)引用的例子:
$a = array( 'one' );
$a[] = &$a;
unset($a);
unset($a)之前的引用關(guān)系:
unset($a)之后的引用關(guān)系:
可以看到菇篡,unset(a)之后由于數(shù)組中有子元素指向 a,所以refcount = 1一喘,此時(shí)是無(wú)法通過(guò)正常的gc機(jī)制回收的驱还,但是$a已經(jīng)已經(jīng)沒(méi)有任何外部引用了,所以這種變量就是垃圾凸克,垃圾回收器要處理的就是這種情況议蟆,這里明確兩個(gè)準(zhǔn)則:
1.如果一個(gè)變量value的refcount減少到0, 那么此value可以被釋放掉萎战,不屬于垃圾
2.如果一個(gè)變量value的refcount減少之后大于0咪鲜,那么此zval還不能被釋放,此zval可能成為一個(gè)垃圾
針對(duì)第一個(gè)情況GC不會(huì)處理撞鹉,只有第二種情況GC才會(huì)將變量收集起來(lái)疟丙。另外變量是否加入垃圾檢查buffer并不是根據(jù)zval的類型判斷的,是通過(guò)zval.u1.type_flag記錄的鸟雏,只有包含IS_TYPE_COLLECTABLE的變量才會(huì)被GC收集享郊。
目前垃圾只會(huì)出現(xiàn)在array、object兩種類型中孝鹊,數(shù)組的情況上面已經(jīng)介紹了炊琉,object的情況則是成員屬性引用對(duì)象本身導(dǎo)致的,其它類型不會(huì)出現(xiàn)這種變量中的成員引用變量自身的情況又活,所以垃圾回收只會(huì)處理這兩種類型的變量苔咪。
回收過(guò)程
如果當(dāng)變量的refcount減少后大于0,PHP并不會(huì)立即進(jìn)行對(duì)這個(gè)變量進(jìn)行垃圾鑒定柳骄,而是放入一個(gè)緩沖buffer中团赏,等這個(gè)buffer滿了以后(默認(rèn)10000個(gè)值)再統(tǒng)一進(jìn)行處理,加入buffer的是變量zend_value的zend_refcounted_h:
typedef struct _zend_refcounted_h {
uint32_t refcount; //記錄zend_value的引用數(shù)
union {
struct {
zend_uchar type, //zend_value的類型,與zval.u1.type一致
zend_uchar flags,
uint16_t gc_info //GC信息耐薯,垃圾回收的過(guò)程會(huì)用到
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;
一個(gè)變量只能加入一次buffer舔清,為了防止重復(fù)加入丝里,變量加入后會(huì)把zend_refcounted_h.gc_info置為GC_PURPLE,即標(biāo)為紫色体谒,下次refcount減少時(shí)如果發(fā)現(xiàn)已經(jīng)加入過(guò)了則不再重復(fù)插入杯聚。
垃圾緩存區(qū)是一個(gè)雙向鏈表,等到緩存區(qū)滿了以后則啟動(dòng)垃圾檢查過(guò)程:遍歷緩存區(qū)抒痒,再對(duì)當(dāng)前變量的所有成員進(jìn)行遍歷幌绍,然后把成員的refcount減1(如果成員還包含子成員則也進(jìn)行遞歸遍歷,其實(shí)就是深度優(yōu)先的遍歷)故响,最后再檢查當(dāng)前變量的引用傀广,如果減為了0則為垃圾。這個(gè)算法的原理很簡(jiǎn)單被去,垃圾是由于成員引用自身導(dǎo)致的主儡,那么就對(duì)所有的成員減一遍引用,結(jié)果如果發(fā)現(xiàn)變量本身refcount變?yōu)榱?則就表明其引用全部來(lái)自自身成員惨缆。具體的過(guò)程如下:
1.從buffer鏈表的roots開(kāi)始遍歷糜值,把當(dāng)前value標(biāo)為灰色(zend_refcounted_h.gc_info置為GC_GREY),然后對(duì)當(dāng)前value的成員進(jìn)行深度優(yōu)先遍歷坯墨,把成員value的refcount減1寂汇,并且也標(biāo)為灰色;
2.重復(fù)遍歷buffer鏈表捣染,檢查當(dāng)前value引用是否為0骄瓣,為0則表示確實(shí)是垃圾,把它標(biāo)為白色(GC_WHITE)耍攘,如果不為0則排除了引用全部來(lái)自自身成員的可能榕栏,表示還有外部的引用,并不是垃圾蕾各,這時(shí)候因?yàn)椴襟E(1)對(duì)成員進(jìn)行了refcount減1操作扒磁,需要再還原回去,對(duì)所有成員進(jìn)行深度遍歷式曲,把成員refcount加1妨托,同時(shí)標(biāo)為黑色;
3.再次遍歷buffer鏈表吝羞,將非GC_WHITE的節(jié)點(diǎn)從roots鏈表中刪除兰伤,最終roots鏈表中全部為真正的垃圾,最后將這些垃圾清除钧排。
垃圾收集的內(nèi)部實(shí)現(xiàn)
接下來(lái)我們簡(jiǎn)單看下垃圾回收的內(nèi)部實(shí)現(xiàn)敦腔,垃圾收集器的全局?jǐn)?shù)據(jù)結(jié)構(gòu):
typedef struct _zend_gc_globals {
zend_bool gc_enabled; //是否啟用gc
zend_bool gc_active; //是否在垃圾檢查過(guò)程中
zend_bool gc_full; //緩存區(qū)是否已滿
gc_root_buffer *buf; //啟動(dòng)時(shí)分配的用于保存可能垃圾的緩存區(qū)
gc_root_buffer roots; //指向buf中最新加入的一個(gè)可能垃圾
gc_root_buffer *unused;//指向buf中沒(méi)有使用的buffer
gc_root_buffer *first_unused; //指向buf中第一個(gè)沒(méi)有使用的buffer
gc_root_buffer *last_unused; //指向buf尾部
gc_root_buffer to_free; //待釋放的垃圾
gc_root_buffer *next_to_free;
uint32_t gc_runs; //統(tǒng)計(jì)gc運(yùn)行次數(shù)
uint32_t collected; //統(tǒng)計(jì)已回收的垃圾數(shù)
} zend_gc_globals;
typedef struct _gc_root_buffer {
zend_refcounted *ref; //每個(gè)zend_value的gc信息
struct _gc_root_buffer *next;
struct _gc_root_buffer *prev;
uint32_t refcount;
} gc_root_buffer;
zend_gc_globals是垃圾回收過(guò)程中主要用到的一個(gè)結(jié)構(gòu),用來(lái)保存垃圾回收器的所有信息卖氨,比如垃圾緩存區(qū)会烙;gc_root_buffer用來(lái)保存每個(gè)可能是垃圾的變量负懦,它實(shí)際就是整個(gè)垃圾收集buffer鏈表的元素筒捺,當(dāng)GC收集一個(gè)變量時(shí)會(huì)創(chuàng)建一個(gè)gc_root_buffer柏腻,插入鏈表。
zend_gc_globals這個(gè)結(jié)構(gòu)中有幾個(gè)關(guān)鍵成員:
1.buf: 前面已經(jīng)說(shuō)過(guò)系吭,當(dāng)refcount減少后如果大于0那么就會(huì)將這個(gè)變量的value加入GC的垃圾緩存區(qū)五嫂,buf就是這個(gè)緩存區(qū),它實(shí)際是一塊連續(xù)的內(nèi)存肯尺,在GC初始化時(shí)一次性分配了10001個(gè)gc_root_buffer沃缘,插入變量時(shí)直接從buf中取出可用節(jié)點(diǎn);
2.roots: 垃圾緩存鏈表的頭部则吟,啟動(dòng)GC檢查的過(guò)程就是從roots開(kāi)始遍歷的槐臀;
3.first_unused: 指向buf中第一個(gè)可用的節(jié)點(diǎn),初始化時(shí)這個(gè)值為1而不是0氓仲,因?yàn)榈谝粋€(gè)gc_root_buffer保留沒(méi)有使用水慨,有元素插入roots時(shí)如果first_unused還沒(méi)有到達(dá)buf的尾部則返回first_unused給最新的元素,然后first_unused++敬扛,直到last_unused晰洒,比如現(xiàn)在已經(jīng)加入了2個(gè)可能的垃圾變量,則對(duì)應(yīng)的結(jié)構(gòu):
4.last_unused: 與first_unused類似啥箭,指向buf末尾
5.unused: GC收集變量時(shí)會(huì)依次從buf中獲取可用的gc_root_buffer谍珊,這種情況直接取first_unused即可,但是有些變量加入垃圾緩存區(qū)之后其refcount又減為0了急侥,這種情況就需要從roots中刪掉砌滞,因?yàn)樗豢赡苁抢@樣就導(dǎo)致roots鏈表并不是像buf分配的那樣是連續(xù)的坏怪,中間會(huì)出現(xiàn)一些開(kāi)始加入后面又刪除的節(jié)點(diǎn)贝润,這些節(jié)點(diǎn)就通過(guò)unused串成一個(gè)單鏈表,unused指向鏈表尾部陕悬,下次有新的變量插入roots時(shí)優(yōu)先使用unused的這些節(jié)點(diǎn)题暖,其次才是first_unused的,舉個(gè)例子
//示例1:
$a = array(); //$a -> zend_array(refcount=1)
$b = $a; //$a -> zend_array(refcount=2)
//$b ->
unset($b); //此時(shí)zend_array(refcount=1)捉超,因?yàn)閞efoucnt>0所以加入gc的垃圾緩存區(qū):roots
unset($a); //此時(shí)zend_array(refcount=0)且gc_info為GC_PURPLE胧卤,則從roots鏈表中刪掉
假如unset($b)時(shí)插入的是buf中第1個(gè)位置,那么unset(a)后對(duì)應(yīng)的結(jié)構(gòu):
如果后面再有變量加入GC垃圾緩存區(qū)將優(yōu)先使用第1個(gè)拼岳。
整理自---《PHP7內(nèi)核剖析》