0. gc的基本結(jié)構(gòu)
0.1 zend_refcounted_h
在《php7的引用計(jì)數(shù)》一文中掸茅,我們說過父腕,php7的復(fù)雜類型弱匪,像字符串、數(shù)組璧亮、引用等的數(shù)據(jù)結(jié)構(gòu)中萧诫,頭部都有一個(gè)gc,變量的引用計(jì)數(shù)維護(hù)在這個(gè)gc中枝嘶。gc是zend_refcounted_h類型的帘饶,其定義如下:
//php7.0 Zend/zend_types.h
typedef struct _zend_refcounted_h {
uint32_t refcount; /* reference counter 32-bit */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type,
zend_uchar flags, /* used for strings & objects */
uint16_t gc_info) /* keeps GC root number (or 0) and color */
} v;
uint32_t type_info;
} u;
} zend_refcounted_h;
struct _zend_refcounted {
zend_refcounted_h gc;
};
typedef struct _zend_refcounted zend_refcounted;
zend_refcounted是由uint32_t的refcount和uint32_t的type_info組成的,總大小為8字節(jié)群扶。type_info中的4字節(jié)(每個(gè)字節(jié)8bit)有著各自的意義及刻,分別如下:
- type:當(dāng)前元素的類型,同zval的u1.v.type竞阐。(為何要冗余記錄一份提茁,我們?cè)诘?部分講解。)
- flags:標(biāo)記數(shù)據(jù)類型馁菜,可以是字符串類型或數(shù)組類型等茴扁。其中標(biāo)記字符串的flags有:
php7.0 /Zend/zend_types.h
/* string flags (zval.value->gc.u.flags) */
#define IS_STR_PERSISTENT (1<<0) /* allocated using malloc */
#define IS_STR_INTERNED (1<<1) /* interned string */
#define IS_STR_PERMANENT (1<<2) /* relives request boundary */
#define IS_STR_CONSTANT (1<<3) /* constant index */
#define IS_STR_CONSTANT_UNQUALIFIED (1<<4) /* the same as IS_CONSTANT_UNQUALIFIED */
標(biāo)記數(shù)組的flags有:
/* array flags */
#define IS_ARRAY_IMMUTABLE (1<<1) /* the same as IS_TYPE_IMMUTABLE */
標(biāo)記對(duì)象的flags有:
/* object flags (zval.value->gc.u.flags) */
#define IS_OBJ_APPLY_COUNT 0x07
#define IS_OBJ_DESTRUCTOR_CALLED (1<<3)
#define IS_OBJ_FREE_CALLED (1<<4)
#define IS_OBJ_USE_GUARDS (1<<5)
#define IS_OBJ_HAS_GUARDS (1<<6)
- gc_info:后面的兩個(gè)字節(jié)標(biāo)記當(dāng)前元素的顏色和垃圾回收池中的位置,其中高地址的兩位用來標(biāo)記顏色汪疮,低地址的14位用于記錄位置峭火。源碼中定義垃圾回收池的大小為100001, 14位可以表示0~16383(2^14-1)智嚷,足夠定義其在回收池中的位置卖丸。
源碼中定義的顏色如下:
//php7.0 Zend/zend_gc.h
#define GC_COLOR 0xc000
#define GC_BLACK 0x0000
#define GC_WHITE 0x8000
#define GC_GREY 0x4000
#define GC_PURPLE 0xc000
色值的取值,剛好配合了使用高最兩位記錄色值設(shè)計(jì)盏道。
zend_refcounted_h的內(nèi)存分布情況如下圖所示稍浆,共占8字節(jié)。
源碼中,色值和地址的取設(shè)均采用了巧妙的位運(yùn)算
//php7.0 Zend/zend_gc.h
/*下面宏中的v為gc.u.v.gc_info*/
//取位置
/*~GC_COLOR為0011 0000 0000 0000, 剛好將v的高兩位顏色位置0, 取到地址衅枫。*/
#define GC_ADDRESS(v) \
((v) & ~GC_COLOR)
//設(shè)置位置
#define GC_INFO_SET_ADDRESS(v, a) \
do {(v) = ((v) & GC_COLOR) | (a);} while (0)
//取顏色
#define GC_INFO_GET_COLOR(v) \
(((zend_uintptr_t)(v)) & GC_COLOR)
//設(shè)置顏色
#define GC_INFO_SET_COLOR(v, c) \
do {(v) = ((v) & ~GC_COLOR) | (c);} while (0)
1. 為何要進(jìn)行垃圾回收~垃圾的產(chǎn)生
對(duì)于php7中復(fù)雜類型, 當(dāng)變量進(jìn)行賦值嫁艇、傳遞時(shí),會(huì)增加其引用數(shù)(不了解的同學(xué)弦撩,可以參看(《php7引用計(jì)數(shù)》)步咪。unset、return 等釋放變量時(shí)再減掉引用數(shù)益楼,減掉后如果發(fā)現(xiàn)引用計(jì)數(shù)變?yōu)?則直接釋放相應(yīng)內(nèi)存猾漫,這是變量的基本回收過程。
不過有一種情況是這個(gè)機(jī)制無法解決的感凤,那就是循環(huán)引用悯周。
什么是循環(huán)引用呢? 簡單的描述就是變量的內(nèi)部成員引用了變量自身陪竿。這種情況常發(fā)生在數(shù)組和對(duì)象類型的變量上队橙。下面我們看一個(gè)例子。
$a = [1];
$a[] = &$a;
unset($a);
在unset之前萨惑,引用關(guān)系如下圖所示:
unset之后引用關(guān)系如下圖所示:
當(dāng)執(zhí)行unset操作后捐康,$a所在的zval類型被標(biāo)記為IS_UNDEF,zend_reference結(jié)構(gòu)體的引用計(jì)數(shù)減1庸蔼,但仍然大于0解总,這時(shí),后面的結(jié)構(gòu)就成為了垃圾姐仅,對(duì)此不處理會(huì)造成內(nèi)存泄露花枫。垃圾回收要處理的就是這種情況。
2. 進(jìn)行垃圾回收的條件
如果一個(gè)變量value的refcount減少之后等于0掏膏,那么此value可以被釋放掉劳翰,不屬于垃圾。GC無需處理馒疹。
如果一個(gè)變量value的refcount減少之后大于0佳簸,那么此zval還不能被釋放,此zval可能成為一個(gè)垃圾颖变。
此時(shí)生均,如果zval.u1.type_flag包含IS_TYPE_COLLECTABLE標(biāo)記,則該變量會(huì)被GC收集并進(jìn)行后續(xù)處理腥刹。
//php7.0 Zend/zend_types.h
#define IS_TYPE_COLLECTABLE (1<<3)
什么類型的變量會(huì)標(biāo)記為IS_TYPE_COLLECTABLE呢马胧?
| type | collectable |
+----------------+-------------+
|simple types | |
|string | |
|interned string | |
|array | Y |
|immutable array | |
|object | Y |
|resource | |
|reference | |
可見目前垃圾回收只針對(duì)array、object兩種類型衔峰。
這也比較好理解佩脊,數(shù)組的情況上面已經(jīng)介紹了蛙粘,object的情況則是成員屬性引用對(duì)象本身導(dǎo)致的,其它類型不會(huì)出現(xiàn)這種變量中的成員引用變量自身的情況威彰,所以垃圾回收只會(huì)處理這兩種類型的變量出牧。
3. 垃圾回收機(jī)制
垃圾回收過程大致分為兩步:
- 將可能是垃圾的變量記錄到垃圾緩存buffer中
- 當(dāng)buffer滿后對(duì)每條記錄進(jìn)行檢查,看是否存在循環(huán)引用的情況抱冷,并進(jìn)行回收崔列。
3.1 垃圾緩存~垃圾收集器
3.1.1 zend_gc_globals
zend_gc_globals是垃圾回收過程中主要用到的一個(gè)結(jié)構(gòu)梢褐,用來保存垃圾回收器的所有信息旺遮,比如垃圾緩存區(qū)。zend_gc_globals的數(shù)據(jù)結(jié)構(gòu)如下:
//php7.0 Zend/zend_gc.h
typedef struct _zend_gc_globals {
zend_bool gc_enabled; //是否啟用gc
zend_bool gc_active; //是否在垃圾檢查過程中
zend_bool gc_full; //緩存區(qū)是否已滿
gc_root_buffer *buf; //啟動(dòng)時(shí)分配的用于保存可能垃圾的緩存區(qū)
gc_root_buffer roots; //指向buf中最新加入的一個(gè)可能垃圾
gc_root_buffer *unused; //指向buf中沒有使用的buffer
gc_root_buffer *first_unused; //指向buf中第一個(gè)沒有使用的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;
說明:
- buf: 當(dāng)refcount減少后如果大于0那么就會(huì)將這個(gè)變量的value加入GC的垃圾緩存區(qū)盈咳,buf就是這個(gè)緩存區(qū)耿眉。它實(shí)際是一塊連續(xù)的內(nèi)存,在GC初始化時(shí)一次性分配了GC_ROOT_BUFFER_MAX_ENTRIES數(shù)量個(gè)gc_root_buffer鱼响,插入變量時(shí)直接從buf中取出可用節(jié)點(diǎn)鸣剪。在php7.0源碼中,GC_ROOT_BUFFER_MAX_ENTRIES值為100001丈积。
//php7.0 Zend/zend_gc.c
#define GC_ROOT_BUFFER_MAX_ENTRIES 10001
- roots: 垃圾緩存鏈表的頭部筐骇,啟動(dòng)GC檢查的過程就是從roots開始遍歷的。
- first_unused: 指向buf中第一個(gè)可用的節(jié)點(diǎn)江滨,初始化時(shí)這個(gè)值為1而不是0铛纬,因?yàn)榈谝粋€(gè)gc_root_buffer保留沒有使用,有元素插入roots時(shí)如果first_unused還沒有到達(dá)buf的尾部則返回first_unused給最新的元素唬滑,然后first_unused++告唆,直到last_unused,比如現(xiàn)在已經(jīng)加入了2個(gè)可能的垃圾變量晶密,則對(duì)應(yīng)的結(jié)構(gòu):
- last_unused: 與first_unused類似擒悬,指向buf末尾。
- unused: 有些變量加入垃圾緩存區(qū)之后其refcount又減為0了稻艰,這種情況就需要從roots中刪掉懂牧,因?yàn)樗豢赡苁抢@樣就導(dǎo)致roots鏈表并不是像buf分配的那樣是連續(xù)的尊勿,中間會(huì)出現(xiàn)一些開始加入后面又刪除的節(jié)點(diǎn)归苍,這些節(jié)點(diǎn)就通過unused串成一個(gè)單鏈表,unused指向鏈表尾部运怖,下次有新的變量插入roots時(shí)優(yōu)先使用unused的這些節(jié)點(diǎn)拼弃,其次才是first_unused的節(jié)點(diǎn)。
下圖是zend_gc_globals結(jié)構(gòu)的內(nèi)存占用情況摇展,總大小為120字節(jié)吻氧。
PHP7中垃圾回收維護(hù)了一個(gè)全局變量gc_globals,存取值的宏為GC_G(v)。
//php7.0 Zend/zend_gc.c
ZEND_API zend_gc_globals gc_globals;
//php7.0 Zend/zend_gc.h
#define GC_G(v) (gc_globals.v)
3.1.2 gc_root_buffer
gc_root_buffer用來保存每個(gè)可能是垃圾的變量盯孙,它實(shí)際就是整個(gè)垃圾收集buffer鏈表的元素鲁森,當(dāng)GC收集一個(gè)變量時(shí)會(huì)創(chuàng)建一個(gè)gc_root_buffer,插入鏈表振惰。gc_root_buffer組成了一個(gè)雙向鏈表歌溉,其數(shù)據(jù)結(jié)構(gòu)如下:
//php7.0 Zend/zend_gc.h
typedef struct _gc_root_buffer {
zend_refcounted *ref; //每個(gè)zend_value的gc信息
struct _gc_root_buffer *next; /* double-linked list*/
struct _gc_root_buffer *prev;
uint32_t refcount;
} gc_root_buffer;
3.1.3 一個(gè)例子
for($i=0; $i<=2; $i++){
$a[$i] = [$i."_string"];
$b[] = $a[$i];
echo "unset $i\n";
unset($a[$i]);
}
unset(i])后,因?yàn)槿匀挥?img class="math-inline" src="https://math.jianshu.com/math?formula=b%E5%AF%B9%E5%BA%94%E7%9A%84%E5%85%83%E7%B4%A0%E6%8C%87%E5%90%91" alt="b對(duì)應(yīng)的元素指向" mathimg="1">a[$i]對(duì)應(yīng)的zend_array骑晶, 所以其引用計(jì)數(shù)不為0痛垛,會(huì)進(jìn)入垃圾回收緩沖區(qū)。相應(yīng)的垃圾收集器的狀態(tài)如下圖所示:
來看一下單個(gè)gc_root_buffer中存儲(chǔ)的數(shù)據(jù)桶蛔。我們知道匙头,zend_array和zend_object結(jié)構(gòu)的第一個(gè)字段都是gc,用于記錄引用計(jì)數(shù)等與垃圾回收相關(guān)的數(shù)據(jù)仔雷。當(dāng)一個(gè)變量可能成為垃圾時(shí)蹂析,其實(shí)gc_root_buffer并不是原樣存儲(chǔ)了一份變量相關(guān)的數(shù)據(jù),而是用一個(gè)ref指針指向了變量數(shù)據(jù)對(duì)應(yīng)的gc字段碟婆。
結(jié)合本例电抚,gc_root_buffer.ref就是指向了zend_array.gc,如下圖所示:
3.1.4 源碼解讀
3.1.4.1 gc_init
垃圾回收器初始化
//7.0.14/Zend/zend_gc.c
ZEND_API void gc_init(void)
{
//buf沒有分配內(nèi)存竖共,且開始了垃圾回收蝙叛,則進(jìn)行內(nèi)存分配和初始化工作
if (GC_G(buf) == NULL && GC_G(gc_enabled)) {
//分配buf緩存區(qū)內(nèi)存,大小為GC_ROOT_BUFFER_MAX_ENTRIES(10001)
GC_G(buf) = (gc_root_buffer*) malloc(sizeof(gc_root_buffer) * GC_ROOT_BUFFER_MAX_ENTRIES);
//last_unused指向緩沖區(qū)末尾
GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES];
gc_reset();
}
}
說明:
- gc_init在php初始化時(shí)就會(huì)被執(zhí)行肘迎。
- 可以在php.ini中設(shè)置zend.enable_gc = On甥温,開啟垃圾回收。
- GC_G是一個(gè)宏妓布,用于獲取全局gc_globals相應(yīng)的字段姻蚓。
- gc_reset()中主要是將gc_globals的各種字段賦初值,比較重要的代碼如下:
//將first_unused指向buf的第一個(gè)節(jié)點(diǎn)匣沼,空出第0個(gè)位置保留狰挡。
GC_G(first_unused) = GC_G(buf) + 1;
3.1.4.2 gc_init
嘗試將變量加入回收緩沖區(qū)。在unset中就調(diào)用了這個(gè)函數(shù)释涛。
先來看看unset的核心代碼
//php7.0 Zend/zend_vm_execute.h
/*針對(duì)變量不同情況加叁,php定義了很多unset,但其核心代碼是類似的*/
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_UNSET_VAR_SPEC_CV_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS){
zend_refcounted *garbage = Z_COUNTED_P(var
//引用計(jì)數(shù)-1, 若為0則直接回收
if (!--GC_REFCOUNT(garbage)) {
ZVAL_UNDEF(var);
zval_dtor_func_for_ptr(garbage);
}
//-1后引用計(jì)數(shù)不為0的情況
else {
zval *z = var;
ZVAL_DEREF(z);
//變量為collectable類型唇撬,且未加入垃圾回收緩存區(qū)
if (Z_COLLECTABLE_P(z) && UNEXPECTED(!Z_GC_INFO_P(z))) {
ZVAL_UNDEF(var);
//嘗試加入緩沖區(qū)
gc_possible_root(Z_COUNTED_P(z));
} else {
ZVAL_UNDEF(var);
}
}
}
接下來是重頭戲它匕,gc_possible_root。
//php7.0.14/Zend/zend_gc.c
//ref參數(shù)窖认,是zend_value相應(yīng)的gc地址
ZEND_API void ZEND_FASTCALL gc_possible_root(zend_refcounted *ref)
{
gc_root_buffer *newRoot;
if (UNEXPECTED(CG(unclean_shutdown)) || UNEXPECTED(GC_G(gc_active))) {
return;
}
//檢查類型豫柬,必須是array或object告希,gc中冗余的type在此處發(fā)揮了作用
ZEND_ASSERT(GC_TYPE(ref) == IS_ARRAY || GC_TYPE(ref) == IS_OBJECT);
//檢查必須是黑色,說明沒有加入過緩沖區(qū)烧给。關(guān)于染色機(jī)制燕偶,在3.2節(jié)中會(huì)詳細(xì)講述。
ZEND_ASSERT(EXPECTED(GC_REF_GET_COLOR(ref) == GC_BLACK));
ZEND_ASSERT(!GC_ADDRESS(GC_INFO(ref)));
//首先嘗試在unused隊(duì)列中取一個(gè)buffer
newRoot = GC_G(unused);
if (newRoot) {
//從unused隊(duì)列中取到一個(gè)buffer, unused后移
GC_G(unused) = newRoot->prev;
}
//buffer隊(duì)列未滿础嫡,則從first_unused取一個(gè)buffer, 同時(shí)將first_unused后移指么。
else if (GC_G(first_unused) != GC_G(last_unused)) {
newRoot = GC_G(first_unused);
GC_G(first_unused)++;
}
//緩沖區(qū)已滿的情況
else {
//未開啟gc,返回
if (!GC_G(gc_enabled)) {
return;
}
//此處為具體的垃圾加收算法榴鼎,將在3.2節(jié)中講述伯诬。
GC_REFCOUNT(ref)++;
gc_collect_cycles();
GC_REFCOUNT(ref)--;
//變量的引用計(jì)數(shù)為0, 直接銷毀
if (UNEXPECTED(GC_REFCOUNT(ref)) == 0) {
zval_dtor_func_for_ptr(ref);
return;
}
//gc.u.v.gc_info有值,說明已加入過buffer檬贰。
if (UNEXPECTED(GC_INFO(ref))) {
return;
}
//垃圾加收后(如果有成功收回的姑廉,則回收的buffer會(huì)加入unused隊(duì)列)缺亮,嘗試從unused取buffer
newRoot = GC_G(unused);
//依然沒有buffer空間翁涤,返回
if (!newRoot) {
return;
}
GC_G(unused) = newRoot->prev;
}
//gc.u.v.gc_info中記錄buf位置和顏色。將變量gc染為紫色萌踱,表明變量已進(jìn)入緩存區(qū)葵礼,染色機(jī)制將在3.2節(jié)詳細(xì)講述。
GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE;
//新的gc_root_buffer.ref指向變量的gc(3.1.3例子的圖示)
newRoot->ref = ref;
//調(diào)整指針并鸵,使得gc_root_buffer加入雙向隊(duì)列
newRoot->next = GC_G(roots).next;
newRoot->prev = &GC_G(roots);
GC_G(roots).next->prev = newRoot;
GC_G(roots).next = newRoot;
}
3.1.5 gdb 深入查看roots鏈上的數(shù)據(jù)鸳粉。
對(duì)于3.1.3中的例子,使用gdb园担,詳細(xì)看下掛接在roots鏈上的數(shù)據(jù)届谈。
在命令行下執(zhí)行g(shù)db php, 進(jìn)入gdb調(diào)試
首先設(shè)置斷點(diǎn)弯汰。
(gdb) b /usr/local/src/php-7.0.14/Zend/zend_gc.c:271
Breakpoint 2 at 0x6664c2: file /usr/local/src/php-7.0.14/Zend/zend_gc.c, line 271.
- zend_gc.c:271 剛好是給gc_possible_root()函數(shù)給newRoot賦完值的位置艰山,停在這里方便我們觀察數(shù)據(jù)。
下面開始調(diào)試
(gdb) run ref.php
Starting program: /search/php70/bin/php ref.php
Breakpoint 1, gc_possible_root (ref=<value optimized out>) at /usr/local/src/php-7.0.14/Zend/zend_gc.c:271
271 GC_G(roots).next->prev = newRoot;
/*注意咏闪,php可能會(huì)有一些自己的變量加入到roots環(huán)曙搬。
*這時(shí)我們需要使用c命令繼續(xù)執(zhí)行,直到看到unset ...的輸出鸽嫂,表明這時(shí)是我們自己代碼的變量進(jìn)入了gc_possible_root纵装。
這也是我們?cè)诖a里加入echo的用途所在。*/
(gdb) c
Continuing.
unset 0
/*有了unset 0据某,此時(shí)是我們的變量進(jìn)入gc_possible_root了*/
Breakpoint 1, gc_possible_root (ref=<value optimized out>) at /usr/local/src/php-7.0.14/Zend/zend_gc.c:271
271 GC_G(roots).next->prev = newRoot;
下面來看newRoot的具體信息
//newRoot就是roots鏈上的元素
(gdb) p newRoot
$2 = (gc_root_buffer *) 0x7ffff7ae9050
//看下元素的內(nèi)容
(gdb) p *newRoot
//ref應(yīng)該指向$a[0]頭上的gc字段
$3 = {ref = 0x7ffff7856230, next = 0x7ffff7ae9030, prev = 0xb13df0, refcount = 0}
(gdb) p *newRoot.ref
/* 引用計(jì)數(shù)為1橡娄,
* type為7,表明類型是數(shù)組癣籽,符合我們的預(yù)期挽唉。
* gc_info為49154, 對(duì)應(yīng)二進(jìn)制1100 0000 0000 0010扳还,紫色,在buf上的第2個(gè)位置
*/
$4 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 49154}, type_info = 3221356551}}}
代碼中橱夭,a[1]進(jìn)入gc_possible_root的情況。
(gdb) c
Continuing.
unset 1
Breakpoint 1, gc_possible_root (ref=<value optimized out>) at /usr/local/src/php-7.0.14/Zend/zend_gc.c:271
271 GC_G(roots).next->prev = newRoot;
(gdb) p *newRoot.ref
/* gc_info為49155, 對(duì)應(yīng)二進(jìn)制1100 0000 0000 0011棘劣,紫色俏让,在buf上的第3個(gè)位置。
* 上一步$a[0]在第2個(gè)位置茬暇,兩者剛好相鄰首昔。
*/
$7 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 49155}, type_info = 3221422087}}}
看下這個(gè)元素的具體內(nèi)容,以確認(rèn)它真的是我們的$a[1]
(gdb) p *(zend_array*)newRoot.ref
/* zend_array的詳情
* arData指向真實(shí)數(shù)據(jù)
* nNumUsed=1:存了一個(gè)元素
*/
$13 = {gc = {refcount = 1, u = {v = {type = 7 '\a', flags = 0 '\000', gc_info = 49155}, type_info = 3221422087}}, u = {v = {
flags = 30 '\036', nApplyCount = 0 '\000', nIteratorsCount = 0 '\000', reserve = 0 '\000'}, flags = 30},
nTableMask = 4294967294, arData = 0x7ffff785cb48, nNumUsed = 1, nNumOfElements = 1, nTableSize = 8, nInternalPointer = 0,
nNextFreeElement = 1, pDestructor = 0x62faa0 <_zval_ptr_dtor>}
(gdb) p ((zend_array*)newRoot.ref).arData[0]
/*查看該元素內(nèi)容糙俗,type為6勒奇,字串類型,符合預(yù)期*/
$14 = {val = {value = {lval = 140737346147544, dval = 6.9533487818369398e-310, counted = 0x7ffff78614d8,
str = 0x7ffff78614d8, arr = 0x7ffff78614d8, obj = 0x7ffff78614d8, res = 0x7ffff78614d8, ref = 0x7ffff78614d8,
ast = 0x7ffff78614d8, zv = 0x7ffff78614d8, ptr = 0x7ffff78614d8, ce = 0x7ffff78614d8, func = 0x7ffff78614d8, ww = {
w1 = 4152759512, w2 = 32767}}, u1 = {v = {type = 6 '\006', type_flags = 20 '\024', const_flags = 0 '\000',
reserved = 0 '\000'}, type_info = 5126}, u2 = {var_flags = 0, next = 0, cache_slot = 0, lineno = 0, num_args = 0,
fe_pos = 0, fe_iter_idx = 0}}, h = 0, key = 0x0}
/*查看字串的存儲(chǔ)
* val為1巧骚,表明字串第一字符為1
* len為8赊颠,表明字串長度為8
*/
(gdb) p *((zend_array*)newRoot.ref).arData[0].val.value.str
$15 = {gc = {refcount = 1, u = {v = {type = 6 '\006', flags = 0 '\000', gc_info = 0}, type_info = 6}}, h = 0, len = 8,
val = "1"}
//打印字串具體內(nèi)容,的確是$a[1]存儲(chǔ)的字串
(gdb) p *((zend_array*)newRoot.ref).arData[0].val.value.str.val@8
$18 = "1_string"
3.2 垃圾回收算法
3.2.1 算法描述
- 遍歷roots鏈表劈彪, 把當(dāng)前元素標(biāo)為灰色(zend_refcounted_h.gc_info置為GC_GREY)竣蹦,然后對(duì)當(dāng)前元素的成員進(jìn)行深度優(yōu)先遍歷,把成員的refcount減1沧奴,并且也標(biāo)為灰色痘括。(gc_mark_roots())
- 遍歷roots鏈表中所有灰色元素及其子元素,如果發(fā)現(xiàn)其引用計(jì)數(shù)仍舊大于0滔吠,說明這個(gè)元素還在其他地方使用纲菌,那么將其顏色重新標(biāo)記會(huì)黑色,并將其引用計(jì)數(shù)加1(在第一步有減1操作)疮绷。如果發(fā)現(xiàn)其引用計(jì)數(shù)為0翰舌,則將其標(biāo)記為白色。(gc_scan_roots())
- 遍歷roots鏈表矗愧,將黑色的元素從roots移除灶芝。然后對(duì)roots中顏色為白色的元素進(jìn)行深度優(yōu)先遍歷,將其引用計(jì)數(shù)加1(在第一步有減1操作)唉韭,同時(shí)將顏色為白色的子元素也加入roots鏈表夜涕。最后然后將roots鏈表移動(dòng)到待釋放的列表to_free中。(gc_collect_roots())
- 釋放to_free列表的元素属愤。
3.2.2 元素顏色轉(zhuǎn)化圖
3.2.3 為什么算法是有效的
為什么算法可以找到垃圾呢女器?我們知道,垃圾產(chǎn)生的原因就是循環(huán)引用住诸,也就是說子元素指向了元素自身驾胆,使用引用計(jì)數(shù)無法清0涣澡。
算法的核心就是嘗試遍歷子元素,將其引用計(jì)數(shù)減1丧诺,若有循環(huán)引用的情況入桂,則在減子元素引用計(jì)數(shù)后,必可使原始元素的引用計(jì)數(shù)清0驳阎。
再來回憶下第一節(jié)這樣循環(huán)引用的圖抗愁。
因?yàn)閦end_array的子元素引用了自身,導(dǎo)致垃圾呵晚。
我們看看算法是如何清除垃圾的:
- 遍歷zend_array蜘腌,對(duì)arData中每個(gè)元素,將其引用計(jì)數(shù)-1饵隙。
- 遍歷到第1個(gè)元素時(shí)撮珠,發(fā)現(xiàn)其指向引用類型。源碼實(shí)現(xiàn)中有這樣一段:
/php-7.0.14/Zend/zend_gc.c
/*如果是引用類型金矛,則將它內(nèi)部的zval對(duì)應(yīng)的數(shù)據(jù)的引用計(jì)數(shù)減1*/
else if (GC_TYPE(ref) == IS_REFERENCE) {
if (Z_REFCOUNTED(((zend_reference*)ref)->val)) {
....
ref = Z_COUNTED(((zend_reference*)ref)->val);
GC_REFCOUNT(ref)--;
...
}
}
對(duì)應(yīng)到我們的例子芯急,就是將zend_array的引用計(jì)數(shù)減1,這時(shí)zend_array的引用計(jì)數(shù)就為0了绷柒,可以回收了志于!
3.2.4 核心代碼解讀
gc_possible_root()中調(diào)用了gc_collect_cycles()來進(jìn)行垃圾回收涮因。gc_collect_cycles是一個(gè)函數(shù)指針废睦, 定義如下:
//php7.0 Zend/zend_gc.c
ZEND_API int (*gc_collect_cycles)(void);
在php-7.0.14/UPGRADING.INTERNALS中有一段說明
gc_collect_cycles() is now a function pointer, and can be replaced in the same manner as zend_execute_ex() if needed (for example, to include the time spent in the garbage collector in a profiler). The default implementation has been renamed to zend_gc_collect_cycles(), and is exported with ZEND_API.
可見zend_gc_collect_cycles默認(rèn)實(shí)現(xiàn)是zend_gc_collect_cycles()的。下面我們就來看下zend_gc_collect_cycles的代碼养泡。
//php7.0 Zend/zend_gc.c
ZEND_API int zend_gc_collect_cycles(void)
{
int count = 0;
/*
*緩存沖區(qū)初始化時(shí)(gc_reset())設(shè)置了 GC_G(roots).next = &GC_G(roots),
*所以只有GC_G(roots).next != &GC_G(roots)才說明roots鏈不空
*/
if (GC_G(roots).next != &GC_G(roots)) {
gc_root_buffer *current, *next, *orig_next_to_free;
zend_refcounted *p;
gc_root_buffer to_free;
uint32_t gc_flags = 0;
gc_additional_buffer *additional_buffer;
/*如果已有回收活動(dòng)正在進(jìn)行嗜湃,則返回*/
if (GC_G(gc_active)) {
return 0;
}
GC_TRACE("Collecting cycles");
GC_G(gc_runs)++;
GC_G(gc_active) = 1;
GC_TRACE("Marking roots");
//遍歷roots鏈表,對(duì)當(dāng)前節(jié)點(diǎn)value的所有成員(如數(shù)組元素澜掩、成員屬性)進(jìn)行深度優(yōu)先遍歷把成員refcount減1
gc_mark_roots();
GC_TRACE("Scanning roots");
/*遍歷roots鏈表中所有灰色元素及其子元素购披,如果發(fā)現(xiàn)其引用計(jì)數(shù)仍舊大于0,說明這個(gè)元素還在其他地方使用肩榕,那么將其顏色重新標(biāo)記會(huì)黑色刚陡,并將其引用計(jì)數(shù)加1。如果發(fā)現(xiàn)其引用計(jì)數(shù)為0株汉,則將其標(biāo)記為白色筐乳。*/
gc_scan_roots();
GC_TRACE("Collecting roots");
additional_buffer = NULL;
/*遍歷roots鏈表,將黑色的元素從roots移除乔妈。
對(duì)roots中顏色為白色的元素進(jìn)行深度優(yōu)先遍歷蝙云,將其引用計(jì)數(shù)加1,同時(shí)將顏色為白色的子元素也加入roots鏈表路召。
最后然后將roots鏈表移動(dòng)到待釋放的列表to_free中勃刨。
關(guān)于additional_buffer波材,在3.2.5節(jié)中做詳細(xì)說明
*/
count = gc_collect_roots(&gc_flags, &additional_buffer);
GC_G(gc_active) = 0;
if (GC_G(to_free).next == &GC_G(to_free)) {
/* nothing to free */
GC_TRACE("Nothing to free");
return 0;
}
/* Copy global to_free list into local list */
to_free.next = GC_G(to_free).next;
to_free.prev = GC_G(to_free).prev;
to_free.next->prev = &to_free;
to_free.prev->next = &to_free;
/* Free global list */
GC_G(to_free).next = &GC_G(to_free);
GC_G(to_free).prev = &GC_G(to_free);
orig_next_to_free = GC_G(next_to_free);
... ...
/*釋放to_free上的垃圾*/
GC_TRACE("Destroying zvals");
GC_G(gc_active) = 1;
current = to_free.next;
while (current != &to_free) {
p = current->ref;
GC_G(next_to_free) = current->next;
GC_TRACE_REF(p, "destroying");
//釋放object
if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_OBJECT) {
zend_object *obj = (zend_object*)p;
...
//調(diào)用free_obj釋放對(duì)象
if (obj->handlers->free_obj) {
GC_REFCOUNT(obj)++;
obj->handlers->free_obj(obj);
GC_REFCOUNT(obj)--;
}
...
}
//釋放數(shù)組
else if ((GC_TYPE(p) & GC_TYPE_MASK) == IS_ARRAY) {
zend_array *arr = (zend_array*)p;
GC_TYPE(arr) = IS_NULL;
zend_hash_destroy(arr);
}
current = GC_G(next_to_free);
}
/*回收使用過的垃圾池buffer,將其放入unused隊(duì)列*/
current = to_free.next;
while (current != &to_free) {
next = current->next;
p = current->ref;
/*
*只有在原有垃圾緩存區(qū)的buffer才可以加入unused
*之所有有此判斷身隐,與additional_buffer相關(guān)
*/
if (EXPECTED(current >= GC_G(buf) && current < GC_G(buf) + GC_ROOT_BUFFER_MAX_ENTRIES)) {
current->prev = GC_G(unused);
GC_G(unused) = current;
}
efree(p);
current = next;
}
//回收additional_buffer的內(nèi)存
while (additional_buffer != NULL) {
gc_additional_buffer *next = additional_buffer->next;
efree(additional_buffer);
additional_buffer = next;
}
GC_TRACE("Collection finished");
GC_G(collected) += count;
GC_G(next_to_free) = orig_next_to_free;
GC_G(gc_active) = 0;
}
return count;
}
3.2.5 gc_additional_buffer
3.2.5.1 gc_additional_buffer的用途
在執(zhí)行g(shù)c_collect_roots()時(shí)廷区,用到了gc_additional_buffer, 這個(gè)結(jié)構(gòu)的用途是什么呢?
通過上面的說明我們知道贾铝,roots上存儲(chǔ)了所有可能是垃圾的元素躲因,但是并沒有存放這些元素的子元素。在
在執(zhí)行g(shù)c_collect_roots()時(shí)忌傻,我們做的很重要的一件事就是將所有白色元素放到roots鏈上大脉,這當(dāng)然也包括白色的子元素。子元素可能有很多水孩,但受限于垃圾緩沖池的大小roots最長只有10000個(gè)镰矿,不夠用怎么辦呢?這時(shí)就需要臨時(shí)申請(qǐng)額外的存儲(chǔ)空間gc_additional_buffer俘种。
3.2.5.2 gc_additional_buffer的結(jié)構(gòu)
gc_additional_buffer結(jié)構(gòu)如下
//Zend/zend_gc.c
typedef struct _gc_addtional_bufer gc_additional_buffer;
struct _gc_addtional_bufer {
uint32_t used;
gc_additional_buffer *next;
gc_root_buffer buf[GC_NUM_ADDITIONAL_ENTRIES];
};
每個(gè)gc_additional_buffer中有GC_NUM_ADDITIONAL_ENTRIES個(gè)gc_root_buffer秤标,可用于存儲(chǔ)待回收的垃圾。當(dāng)一個(gè)gc_additional_buffer不夠用時(shí)宙刘,就會(huì)再申請(qǐng)一個(gè)gc_additional_buffer, 多個(gè)gc_additional_buffer使用next指針串連苍姜,形成鏈表。
3.2.5.3 gc_additional_buffer的具體使用
gc_additional_buffer在gc_add_garbage()中使用悬包,gc_add_garbage的功能是將不在roots鏈上的白色元素掛接到roots鏈上衙猪。
調(diào)用棧如下:
gc_add_garbage()
gc_collect_white()
gc_collect_roots()
下面來具體看下gc_add_garbage的實(shí)現(xiàn)
static void gc_add_garbage(zend_refcounted *ref, gc_additional_buffer **additional_buffer){
//首先嘗試從unused鏈上取buffer
gc_root_buffer *buf = GC_G(unused);
if (buf) {
GC_G(unused) = buf->prev;
/* optimization: color is already GC_BLACK (0) */
//記錄buf在緩沖池中的位置
GC_INFO(ref) = buf - GC_G(buf);
}
//接下來嘗試從first_unused取一個(gè)buffer
else if (GC_G(first_unused) != GC_G(last_unused)) {
buf = GC_G(first_unused);
GC_G(first_unused)++;
//記錄buf在緩沖池中的位置
GC_INFO(ref) = buf - GC_G(buf);
}
//現(xiàn)有垃圾回收池滿了
else {
/* If we don't have free slots in the buffer, allocate a new one and
* set it's address to GC_ROOT_BUFFER_MAX_ENTRIES that have special meaning.
*/
//沒有additional_buffer或者當(dāng)前additional_buffer已裝滿
if (!*additional_buffer || (*additional_buffer)->used == GC_NUM_ADDITIONAL_ENTRIES) {
//新申請(qǐng)內(nèi)存裝初始化一個(gè)additional_buffer
gc_additional_buffer *new_buffer = emalloc(sizeof(gc_additional_buffer));
new_buffer->used = 0;
new_buffer->next = *additional_buffer;
*additional_buffer = new_buffer;
}
//從當(dāng)前additional_buffe上取一個(gè)buffer
buf = (*additional_buffer)->buf + (*additional_buffer)->used;
(*additional_buffer)->used++;
/*
* 將buf位置記錄為GC_ROOT_BUFFER_MAX_ENTRIES
* 注意GC_ROOT_BUFFER_MAX_ENTRIES是不存在于原有垃圾緩沖區(qū)的一個(gè)位置
*/
GC_INFO(ref) = GC_ROOT_BUFFER_MAX_ENTRIES;
/* modify type to prevent indirect destruction */
GC_TYPE(ref) |= GC_FAKE_BUFFER_FLAG;
}
//取到buffer, 記錄信息并將其掛接到roots鏈
if (buf) {
GC_REFCOUNT(ref)++;
buf->ref = ref;
buf->next = GC_G(roots).next;
buf->prev = &GC_G(roots);
GC_G(roots).next->prev = buf;
GC_G(roots).next = buf;
}
}
4. 再說gc結(jié)構(gòu)
為什么gc要放在復(fù)雜變量的頭部?為什么zval中有變量類型布近,gc中要再記錄一份垫释?
回憶一下php7中變量的存儲(chǔ)方式,
一個(gè)zval中包含一個(gè)zend_value結(jié)構(gòu)撑瞧,zend_value中相應(yīng)類型的指針指向?qū)?yīng)類型的實(shí)際存儲(chǔ)空間棵譬。
一個(gè)array類形的變量,存儲(chǔ)方式如下圖所示预伺。
zend_value中的arr指向了zend_array订咸。
在垃圾處理過程中,我們主要用到的都是指向gc的指針酬诀。但是在染色時(shí)脏嚷,我們需根據(jù)變量類型對(duì)變量內(nèi)部存儲(chǔ)的子元素。這時(shí)怎么辦呢料滥?看垃圾加收過程中的代碼:
//php7.0 Zend/zend_gc.c
static void gc_mark_grey(zend_refcounted *ref)
{
HashTable *ht;
Bucket *p, *end;
zval *zv;
if (GC_REF_GET_COLOR(ref) != GC_GREY) {
//染成灰色
GC_REF_SET_COLOR(ref, GC_GREY);
if (GC_TYPE(ref) == IS_OBJECT) {
zend_object_get_gc_t get_gc;
//轉(zhuǎn)換為zend_object類型
zend_object *obj = (zend_object*)ref;
... ...
}
else if (GC_TYPE(ref) == IS_ARRAY) {
...
//轉(zhuǎn)換為zend_array類型
ht = (zend_array*)ref;
...
}
}
}
... ...
}
這段代碼的功能是將元素及其子元素染成灰色然眼,由gc_mark_roots()調(diào)用。它接收的參數(shù)ref變是一個(gè)指向gc的指針葵腹。
GC_TYPE(ref)用于獲取變量類型高每。這也是為什么zval中記錄了變量類型屿岂,我們?nèi)匀灰趃c中冗余一份的原因。
#define GC_TYPE(p) (p)->gc.u.v.type
因?yàn)間c在相應(yīng)數(shù)據(jù)類型的起始位置鲸匿,所以爷怀,在知道具體類型后,我們只要使用強(qiáng)制類型轉(zhuǎn)換带欢,就可以將指向gc類型的指向轉(zhuǎn)為指向具體類型的指針运授,并通過類型指針取到變量具體數(shù)據(jù)。
zend_object *obj = (zend_object*)ref;
ht = (zend_array*)ref;