PHP5.3的垃圾回收機(jī)制(動(dòng)態(tài)存儲(chǔ)分配方案)深入理解
更新時(shí)間:2012年12月10日 09:56:56 作者:
垃圾回收機(jī)制是一種動(dòng)態(tài)存儲(chǔ)分配方案,它會(huì)自動(dòng)釋放程序不再需要的已分配的內(nèi)存塊,PHP也在語言層實(shí)現(xiàn)了內(nèi)存的動(dòng)態(tài)管理.內(nèi)存的動(dòng)態(tài)管理將開發(fā)人員從繁瑣的內(nèi)存管理中解救出來
垃圾回收機(jī)制是一種動(dòng)態(tài)存儲(chǔ)分配方案。它會(huì)自動(dòng)釋放程序不再需要的已分配的內(nèi)存塊。 自動(dòng)回收內(nèi)存的過程叫垃圾收集。垃圾回收機(jī)制可以讓程序員不必過分關(guān)心程序內(nèi)存分配,從而將更多的精力投入到業(yè)務(wù)邏輯。 在現(xiàn)在的流行各種語言當(dāng)中,垃圾回收機(jī)制是新一代語言所共有的特征,如Python、PHP、Eiffel、C#、Ruby等都使用了垃圾回收機(jī)制。 雖然垃圾回收是現(xiàn)在比較流行的做法,但是它的年紀(jì)已經(jīng)不小了。早在20世紀(jì)60年代MIT開發(fā)的Lisp系統(tǒng)中就已經(jīng)有了它的身影, 但是由于當(dāng)時(shí)技術(shù)條件不成熟,從而使得垃圾回收機(jī)制成了一個(gè)看起來很美的技術(shù),直到20世紀(jì)90年代Java的出現(xiàn),垃圾回收機(jī)制才被廣泛應(yīng)用。
PHP也在語言層實(shí)現(xiàn)了內(nèi)存的動(dòng)態(tài)管理,這在前面的章節(jié)中已經(jīng)有了詳細(xì)的說明, 內(nèi)存的動(dòng)態(tài)管理將開發(fā)人員從繁瑣的內(nèi)存管理中解救出來。與此配套,PHP也提供了語言層的垃圾回收機(jī)制, 讓程序員不必過分關(guān)心程序內(nèi)存分配。
在PHP5.3版本之前,PHP只有簡(jiǎn)單的基于引用計(jì)數(shù)的垃圾回收,當(dāng)一個(gè)變量的引用計(jì)數(shù)變?yōu)?時(shí), PHP將在內(nèi)存中銷毀這個(gè)變量,只是這里的垃圾并不能稱之為垃圾。 并且PHP在一個(gè)生命周期結(jié)束后就會(huì)釋放此進(jìn)程/線程所點(diǎn)的內(nèi)容,這種方式?jīng)Q定了PHP在前期不需要過多考慮內(nèi)存的泄露問題。 但是隨著PHP的發(fā)展,PHP開發(fā)者的增加以及其所承載的業(yè)務(wù)范圍的擴(kuò)大,在PHP5.3中引入了更加完善的垃圾回收機(jī)制。 新的垃圾回收機(jī)制解決了無法處理循環(huán)的引用內(nèi)存泄漏問題。PHP5.3中的垃圾回收機(jī)制使用了文章引用計(jì)數(shù)系統(tǒng)中的同步周期回收(Concurrent Cycle Collection in Reference Counted Systems) 中的同步算法。關(guān)于這個(gè)算法的介紹我們就不再贅述,在PHP的官方文檔有圖文并茂的介紹:回收周期(Collecting Cycles)。
如前面所說,在PHP中,主要的內(nèi)存管理手段是引用計(jì)數(shù),引入垃圾收集機(jī)制的目的是為了打破引用計(jì)數(shù)中的循環(huán)引用,從而防止因?yàn)檫@個(gè)而產(chǎn)生的內(nèi)存泄露。 垃圾收集機(jī)制基于PHP的動(dòng)態(tài)內(nèi)存管理而存在。PHP5.3為引入垃圾收集機(jī)制,在變量存儲(chǔ)的基本結(jié)構(gòu)上有一些變動(dòng),如下所示:
struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};
與PHP5.3之前的版本相比,引用計(jì)數(shù)字段refcount和是否引用字段is_ref都在其后面添加了__gc以用于新的的垃圾回收機(jī)制。 在PHP的源碼風(fēng)格中,大量的宏是一個(gè)非常鮮明的特點(diǎn)。這些宏相當(dāng)于一個(gè)接口層,它屏蔽了接口層以下的一些底層實(shí)現(xiàn),如, ALLOC_ZVAL宏,這個(gè)宏在PHP5.3之前是直接調(diào)用PHP的內(nèi)存管理分配函數(shù)emalloc分配內(nèi)存,所分配的內(nèi)存大小由變量的類型等大小決定。 在引入垃圾回收機(jī)制后,ALLOC_ZVAL宏直接采用新的垃圾回收單元結(jié)構(gòu),所分配的大小都是一樣的,全部是zval_gc_info結(jié)構(gòu)體所占內(nèi)存大小, 并且在分配內(nèi)存后,初始化這個(gè)結(jié)構(gòu)體的垃圾回收機(jī)制。如下代碼:
/* The following macroses override macroses from zend_alloc.h */
#undef ALLOC_ZVAL
#define ALLOC_ZVAL(z) \
do { \
(z) = (zval*)emalloc(sizeof(zval_gc_info)); \
GC_ZVAL_INIT(z); \
} while (0)
zend_gc.h文件在zend.h的749行被引用:#include “zend_gc.h” 從而替換覆蓋了在237行引用的zend_alloc.h文件中的ALLOC_ZVAL等宏 在新的的宏中,關(guān)鍵性的改變是對(duì)所分配內(nèi)存大小和分配內(nèi)容的改變,在以前純粹的內(nèi)存分配中添加了垃圾收集機(jī)制的內(nèi)容, 所有的內(nèi)容都包括在zval_gc_info結(jié)構(gòu)體中:
typedef struct _zval_gc_info {
zval z;
union {
gc_root_buffer *buffered;
struct _zval_gc_info *next;
} u;
} zval_gc_info;
對(duì)于任何一個(gè)ZVAL容器存儲(chǔ)的變量,分配了一個(gè)zval結(jié)構(gòu),這個(gè)結(jié)構(gòu)確保其和以zval變量分配的內(nèi)存的開始對(duì)齊, 從而在zval_gc_info類型指針的強(qiáng)制轉(zhuǎn)換時(shí),其可以作為zval使用。在zval字段后面有一個(gè)聯(lián)合體:u。 u包括gc_root_buffer結(jié)構(gòu)的buffered字段和zval_gc_info結(jié)構(gòu)的next字段。 這兩個(gè)字段一個(gè)是表示垃圾收集機(jī)制緩存的根結(jié)點(diǎn),一個(gè)是zval_gc_info列表的下一個(gè)結(jié)點(diǎn), 垃圾收集機(jī)制緩存的結(jié)點(diǎn)無論是作為根結(jié)點(diǎn),還是列表結(jié)點(diǎn),都可以在這里體現(xiàn)。 ALLOC_ZVAL在分配了內(nèi)存后會(huì)調(diào)用GC_ZVAL_INIT用來初始化替代了zval的zval_gc_info, 它會(huì)把zval_gc_info中的成員u的buffered字段設(shè)置成NULL,此字段僅在將其放入垃圾回收緩沖區(qū)時(shí)才會(huì)有值,否則會(huì)一直是NULL。 由于PHP中所有的變量都是以zval變量的形式存在,這里以zval_gc_info替換zval,從而成功實(shí)現(xiàn)垃圾收集機(jī)制在原有系統(tǒng)中的集成。
PHP的垃圾回收機(jī)制在PHP5.3中默認(rèn)為開啟,但是我們可以通過配置文件直接設(shè)置為禁用,其對(duì)應(yīng)的配置字段為:zend.enable_gc。 在php.ini文件中默認(rèn)是沒有這個(gè)字段的,如果我們需要禁用此功能,則在php.ini中添加zend.enable_gc=0或zend.enable_gc=off。 除了修改php.ini配置zend.enable_gc,也可以通過調(diào)用gc_enable()/gc_disable()函數(shù)來打開/關(guān)閉垃圾回收機(jī)制。 這些函數(shù)的調(diào)用效果與修改配置項(xiàng)來打開或關(guān)閉垃圾回收機(jī)制的效果是一樣的。 除了這兩個(gè)函數(shù)PHP提供了gc_collect_cycles()函數(shù)可以在根緩沖區(qū)還沒滿時(shí)強(qiáng)制執(zhí)行周期回收。 與垃圾回收機(jī)制是否開啟在PHP源碼中有一些相關(guān)的操作和字段。在zend.c文件中有如下代碼:
static ZEND_INI_MH(OnUpdateGCEnabled) /* {{{ */
{
OnUpdateBool(entry, new_value, new_value_length, mh_arg1, mh_arg2, mh_arg3, stage TSRMLS_CC);
if (GC_G(gc_enabled)) {
gc_init(TSRMLS_C);
}
return SUCCESS;
}
/* }}} */
ZEND_INI_BEGIN()
ZEND_INI_ENTRY("error_reporting", NULL, ZEND_INI_ALL, OnUpdateErrorReporting)
STD_ZEND_INI_BOOLEAN("zend.enable_gc", "1", ZEND_INI_ALL, OnUpdateGCEnabled, gc_enabled, zend_gc_globals, gc_globals)
#ifdef ZEND_MULTIBYTE
STD_ZEND_INI_BOOLEAN("detect_unicode", "1", ZEND_INI_ALL, OnUpdateBool, detect_unicode, zend_compiler_globals, compiler_globals)
#endif
ZEND_INI_END()
zend.enable_gc對(duì)應(yīng)的操作函數(shù)為ZEND_INI_MH(OnUpdateGCEnabled),如果開啟了垃圾回收機(jī)制, 即GC_G(gc_enabled)為真,則會(huì)調(diào)用gc_init函數(shù)執(zhí)行垃圾回收機(jī)制的初始化操作。 gc_init函數(shù)在zend/zend_gc.c 121行,此函數(shù)會(huì)判斷是否開啟垃圾回收機(jī)制, 如果開啟,則初始化整個(gè)機(jī)制,即直接調(diào)用malloc給整個(gè)緩存列表分配10000個(gè)gc_root_buffer內(nèi)存空間。 這里的10000是硬編碼在代碼中的,以宏GC_ROOT_BUFFER_MAX_ENTRIES存在,如果需要修改這個(gè)值,則需要修改源碼,重新編譯PHP。 gc_init函數(shù)在預(yù)分配內(nèi)存后調(diào)用gc_reset函數(shù)重置整個(gè)機(jī)制用到的一些全局變量,如設(shè)置gc運(yùn)行的次數(shù)統(tǒng)計(jì)(gc_runs)和gc中垃圾的個(gè)數(shù)(collected)為0, 設(shè)置雙向鏈表頭結(jié)點(diǎn)的上一個(gè)結(jié)點(diǎn)和下一個(gè)結(jié)點(diǎn)指向自己等。除了這種提的一些用于垃圾回收機(jī)制的全局變量,還有其它一些使用較多的變量,部分說明如下:
typedef struct _zend_gc_globals {
zend_bool gc_enabled; /* 是否開啟垃圾收集機(jī)制 */
zend_bool gc_active; /* 是否正在進(jìn)行 */
gc_root_buffer *buf; /* 預(yù)分配的緩沖區(qū)數(shù)組,默認(rèn)為10000(preallocated arrays of buffers) */
gc_root_buffer roots; /* 列表的根結(jié)點(diǎn)(list of possible roots of cycles) */
gc_root_buffer *unused; /* 沒有使用過的緩沖區(qū)列表(list of unused buffers) */
gc_root_buffer *first_unused; /* 指向第一個(gè)沒有使用過的緩沖區(qū)結(jié)點(diǎn)(pointer to first unused buffer) */
gc_root_buffer *last_unused; /* 指向最后一個(gè)沒有使用過的緩沖區(qū)結(jié)點(diǎn),此處為標(biāo)記結(jié)束用(pointer to last unused buffer) */
zval_gc_info *zval_to_free; /* 將要釋放的zval變量的臨時(shí)列表(temporaryt list of zvals to free) */
zval_gc_info *free_list; /* 臨時(shí)變量,需要釋放的列表開頭 */
zval_gc_info *next_to_free; /* 臨時(shí)變量,下一個(gè)將要釋放的變量位置*/
zend_uint gc_runs; /* gc運(yùn)行的次數(shù)統(tǒng)計(jì) */
zend_uint collected; /* gc中垃圾的個(gè)數(shù) */
// 省略...
}
當(dāng)我們使用一個(gè)unset操作想清除這個(gè)變量所占的內(nèi)存時(shí)(可能只是引用計(jì)數(shù)減一),會(huì)從當(dāng)前符號(hào)的哈希表中刪除變量名對(duì)應(yīng)的項(xiàng), 在所有的操作執(zhí)行完后,并對(duì)從符號(hào)表中刪除的項(xiàng)調(diào)用一個(gè)析構(gòu)函數(shù),臨時(shí)變量會(huì)調(diào)用zval_dtor,一般的變量會(huì)調(diào)用zval_ptr_dtor。
當(dāng)然我們無法在PHP的函數(shù)集中找到unset函數(shù),因?yàn)樗且环N語言結(jié)構(gòu)。 其對(duì)應(yīng)的中間代碼為ZEND_UNSET,在Zend/zend_vm_execute.h文件中你可以找到與它相關(guān)的實(shí)現(xiàn)。
zval_ptr_dtor并不是一個(gè)函數(shù),只是一個(gè)長(zhǎng)得有點(diǎn)像函數(shù)的宏。 在Zend/zend_variables.h文件中,這個(gè)宏指向函數(shù)_zval_ptr_dtor。 在Zend/zend_execute_API.c 424行,函數(shù)相關(guān)代碼如下:
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);
}
}
/* }}} */
從代碼我們可以很清晰的看出這個(gè)zval的析構(gòu)過程,關(guān)于引用計(jì)數(shù)字段做了以下兩個(gè)操作:
如果變量的引用計(jì)數(shù)為1,即減一后引用計(jì)數(shù)為0,直接清除變量。如果當(dāng)前變量如果被緩存,則需要清除緩存如果變量的引用計(jì)數(shù)大于1,即減一后引用計(jì)數(shù)大于0,則將變量放入垃圾列表。如果變更存在引用,則去掉其引用。
將變量放入垃圾列表的操作是GC_ZVAL_CHECK_POSSIBLE_ROOT,這也是一個(gè)宏,其對(duì)應(yīng)函數(shù)gc_zval_check_possible_root, 但是此函數(shù)僅對(duì)數(shù)組和對(duì)象執(zhí)行垃圾回收操作。對(duì)于數(shù)組和對(duì)象變量,它會(huì)調(diào)用gc_zval_possible_root函數(shù)。
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函數(shù)僅對(duì)數(shù)組和對(duì)象執(zhí)行垃圾回收操作,然而在gc_zval_possible_root函數(shù)中, 針對(duì)對(duì)象類型的變量會(huì)去調(diào)用GC_ZOBJ_CHECK_POSSIBLE_ROOT宏。而對(duì)于其它的可用于垃圾回收的機(jī)制的變量類型其調(diào)用過程如下:
檢查zval結(jié)點(diǎn)信息是否已經(jīng)放入到結(jié)點(diǎn)緩沖區(qū),如果已經(jīng)放入到結(jié)點(diǎn)緩沖區(qū),則直接返回,這樣可以優(yōu)化其性能。 然后處理對(duì)象結(jié)點(diǎn),直接返回,不再執(zhí)行后面的操作判斷結(jié)點(diǎn)是否已經(jīng)被標(biāo)記為紫色,如果為紫色則不再添加到結(jié)點(diǎn)緩沖區(qū),此處在于保證一個(gè)結(jié)點(diǎn)只執(zhí)行一次添加到緩沖區(qū)的操作。
將結(jié)點(diǎn)的顏色標(biāo)記為紫色,表示此結(jié)點(diǎn)已經(jīng)添加到緩沖區(qū),下次不用再做添加
找出新的結(jié)點(diǎn)的位置,如果緩沖區(qū)滿了,則執(zhí)行垃圾回收操作。
將新的結(jié)點(diǎn)添加到緩沖區(qū)所在的雙向鏈表。
在gc_zval_possible_root函數(shù)中,當(dāng)緩沖區(qū)滿時(shí),程序調(diào)用gc_collect_cycles函數(shù),執(zhí)行垃圾回收操作。 其中最關(guān)鍵的幾步就是:
第628行 此處為其官方文檔中算法的步驟 B ,算法使用深度優(yōu)先搜索查找所有可能的根,找到后將每個(gè)變量容器中的引用計(jì)數(shù)減1, 為確保不會(huì)對(duì)同一個(gè)變量容器減兩次“1”,用灰色標(biāo)記已減過1的。
第629行 這是算法的步驟 C ,算法再一次對(duì)每個(gè)根節(jié)點(diǎn)使用深度優(yōu)先搜索,檢查每個(gè)變量容器的引用計(jì)數(shù)。 如果引用計(jì)數(shù)是 0 ,變量容器用白色來標(biāo)記。如果引用次數(shù)大于0,則恢復(fù)在這個(gè)點(diǎn)上使用深度優(yōu)先搜索而將引用計(jì)數(shù)減1的操作(即引用計(jì)數(shù)加1), 然后將它們重新用黑色標(biāo)記。
第630行 算法的最后一步 D ,算法遍歷根緩沖區(qū)以從那里刪除變量容器根(zval roots), 同時(shí),檢查是否有在上一步中被白色標(biāo)記的變量容器。每個(gè)被白色標(biāo)記的變量容器都被清除。 在[gc_collect_cycles() -> gc_collect_roots() -> zval_collect_white() ]中我們可以看到, 對(duì)于白色標(biāo)記的結(jié)點(diǎn)會(huì)被添加到全局變量zval_to_free列表中。此列表在后面的操作中有用到。
PHP的垃圾回收機(jī)制在執(zhí)行過程中以四種顏色標(biāo)記狀態(tài)。
GC_WHITE 白色表示垃圾
GC_PURPLE 紫色表示已放入緩沖區(qū)
GC_GREY 灰色表示已經(jīng)進(jìn)行了一次refcount的減一操作
GC_BLACK 黑色是默認(rèn)顏色,正常
相關(guān)的標(biāo)記以及操作代碼如下:
#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))
以上的這種以位來標(biāo)記狀態(tài)的方式在PHP的源碼中使用頻率較高,如內(nèi)存管理等都有用到, 這是一種比較高效及節(jié)省的方案。但是在我們做數(shù)據(jù)庫設(shè)計(jì)時(shí)可能對(duì)于字段不能使用這種方式, 應(yīng)該是以一種更加直觀,更加具有可讀性的方式實(shí)現(xiàn)。
PHP也在語言層實(shí)現(xiàn)了內(nèi)存的動(dòng)態(tài)管理,這在前面的章節(jié)中已經(jīng)有了詳細(xì)的說明, 內(nèi)存的動(dòng)態(tài)管理將開發(fā)人員從繁瑣的內(nèi)存管理中解救出來。與此配套,PHP也提供了語言層的垃圾回收機(jī)制, 讓程序員不必過分關(guān)心程序內(nèi)存分配。
在PHP5.3版本之前,PHP只有簡(jiǎn)單的基于引用計(jì)數(shù)的垃圾回收,當(dāng)一個(gè)變量的引用計(jì)數(shù)變?yōu)?時(shí), PHP將在內(nèi)存中銷毀這個(gè)變量,只是這里的垃圾并不能稱之為垃圾。 并且PHP在一個(gè)生命周期結(jié)束后就會(huì)釋放此進(jìn)程/線程所點(diǎn)的內(nèi)容,這種方式?jīng)Q定了PHP在前期不需要過多考慮內(nèi)存的泄露問題。 但是隨著PHP的發(fā)展,PHP開發(fā)者的增加以及其所承載的業(yè)務(wù)范圍的擴(kuò)大,在PHP5.3中引入了更加完善的垃圾回收機(jī)制。 新的垃圾回收機(jī)制解決了無法處理循環(huán)的引用內(nèi)存泄漏問題。PHP5.3中的垃圾回收機(jī)制使用了文章引用計(jì)數(shù)系統(tǒng)中的同步周期回收(Concurrent Cycle Collection in Reference Counted Systems) 中的同步算法。關(guān)于這個(gè)算法的介紹我們就不再贅述,在PHP的官方文檔有圖文并茂的介紹:回收周期(Collecting Cycles)。
如前面所說,在PHP中,主要的內(nèi)存管理手段是引用計(jì)數(shù),引入垃圾收集機(jī)制的目的是為了打破引用計(jì)數(shù)中的循環(huán)引用,從而防止因?yàn)檫@個(gè)而產(chǎn)生的內(nèi)存泄露。 垃圾收集機(jī)制基于PHP的動(dòng)態(tài)內(nèi)存管理而存在。PHP5.3為引入垃圾收集機(jī)制,在變量存儲(chǔ)的基本結(jié)構(gòu)上有一些變動(dòng),如下所示:
復(fù)制代碼 代碼如下:
struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
zend_uint refcount__gc;
zend_uchar type; /* active type */
zend_uchar is_ref__gc;
};
與PHP5.3之前的版本相比,引用計(jì)數(shù)字段refcount和是否引用字段is_ref都在其后面添加了__gc以用于新的的垃圾回收機(jī)制。 在PHP的源碼風(fēng)格中,大量的宏是一個(gè)非常鮮明的特點(diǎn)。這些宏相當(dāng)于一個(gè)接口層,它屏蔽了接口層以下的一些底層實(shí)現(xiàn),如, ALLOC_ZVAL宏,這個(gè)宏在PHP5.3之前是直接調(diào)用PHP的內(nèi)存管理分配函數(shù)emalloc分配內(nèi)存,所分配的內(nèi)存大小由變量的類型等大小決定。 在引入垃圾回收機(jī)制后,ALLOC_ZVAL宏直接采用新的垃圾回收單元結(jié)構(gòu),所分配的大小都是一樣的,全部是zval_gc_info結(jié)構(gòu)體所占內(nèi)存大小, 并且在分配內(nèi)存后,初始化這個(gè)結(jié)構(gòu)體的垃圾回收機(jī)制。如下代碼:
復(fù)制代碼 代碼如下:
/* The following macroses override macroses from zend_alloc.h */
#undef ALLOC_ZVAL
#define ALLOC_ZVAL(z) \
do { \
(z) = (zval*)emalloc(sizeof(zval_gc_info)); \
GC_ZVAL_INIT(z); \
} while (0)
zend_gc.h文件在zend.h的749行被引用:#include “zend_gc.h” 從而替換覆蓋了在237行引用的zend_alloc.h文件中的ALLOC_ZVAL等宏 在新的的宏中,關(guān)鍵性的改變是對(duì)所分配內(nèi)存大小和分配內(nèi)容的改變,在以前純粹的內(nèi)存分配中添加了垃圾收集機(jī)制的內(nèi)容, 所有的內(nèi)容都包括在zval_gc_info結(jié)構(gòu)體中:
復(fù)制代碼 代碼如下:
typedef struct _zval_gc_info {
zval z;
union {
gc_root_buffer *buffered;
struct _zval_gc_info *next;
} u;
} zval_gc_info;
對(duì)于任何一個(gè)ZVAL容器存儲(chǔ)的變量,分配了一個(gè)zval結(jié)構(gòu),這個(gè)結(jié)構(gòu)確保其和以zval變量分配的內(nèi)存的開始對(duì)齊, 從而在zval_gc_info類型指針的強(qiáng)制轉(zhuǎn)換時(shí),其可以作為zval使用。在zval字段后面有一個(gè)聯(lián)合體:u。 u包括gc_root_buffer結(jié)構(gòu)的buffered字段和zval_gc_info結(jié)構(gòu)的next字段。 這兩個(gè)字段一個(gè)是表示垃圾收集機(jī)制緩存的根結(jié)點(diǎn),一個(gè)是zval_gc_info列表的下一個(gè)結(jié)點(diǎn), 垃圾收集機(jī)制緩存的結(jié)點(diǎn)無論是作為根結(jié)點(diǎn),還是列表結(jié)點(diǎn),都可以在這里體現(xiàn)。 ALLOC_ZVAL在分配了內(nèi)存后會(huì)調(diào)用GC_ZVAL_INIT用來初始化替代了zval的zval_gc_info, 它會(huì)把zval_gc_info中的成員u的buffered字段設(shè)置成NULL,此字段僅在將其放入垃圾回收緩沖區(qū)時(shí)才會(huì)有值,否則會(huì)一直是NULL。 由于PHP中所有的變量都是以zval變量的形式存在,這里以zval_gc_info替換zval,從而成功實(shí)現(xiàn)垃圾收集機(jī)制在原有系統(tǒng)中的集成。
PHP的垃圾回收機(jī)制在PHP5.3中默認(rèn)為開啟,但是我們可以通過配置文件直接設(shè)置為禁用,其對(duì)應(yīng)的配置字段為:zend.enable_gc。 在php.ini文件中默認(rèn)是沒有這個(gè)字段的,如果我們需要禁用此功能,則在php.ini中添加zend.enable_gc=0或zend.enable_gc=off。 除了修改php.ini配置zend.enable_gc,也可以通過調(diào)用gc_enable()/gc_disable()函數(shù)來打開/關(guān)閉垃圾回收機(jī)制。 這些函數(shù)的調(diào)用效果與修改配置項(xiàng)來打開或關(guān)閉垃圾回收機(jī)制的效果是一樣的。 除了這兩個(gè)函數(shù)PHP提供了gc_collect_cycles()函數(shù)可以在根緩沖區(qū)還沒滿時(shí)強(qiáng)制執(zhí)行周期回收。 與垃圾回收機(jī)制是否開啟在PHP源碼中有一些相關(guān)的操作和字段。在zend.c文件中有如下代碼:
復(fù)制代碼 代碼如下:
static ZEND_INI_MH(OnUpdateGCEnabled) /* {{{ */
{
OnUpdateBool(entry, new_value, new_value_length, mh_arg1, mh_arg2, mh_arg3, stage TSRMLS_CC);
if (GC_G(gc_enabled)) {
gc_init(TSRMLS_C);
}
return SUCCESS;
}
/* }}} */
ZEND_INI_BEGIN()
ZEND_INI_ENTRY("error_reporting", NULL, ZEND_INI_ALL, OnUpdateErrorReporting)
STD_ZEND_INI_BOOLEAN("zend.enable_gc", "1", ZEND_INI_ALL, OnUpdateGCEnabled, gc_enabled, zend_gc_globals, gc_globals)
#ifdef ZEND_MULTIBYTE
STD_ZEND_INI_BOOLEAN("detect_unicode", "1", ZEND_INI_ALL, OnUpdateBool, detect_unicode, zend_compiler_globals, compiler_globals)
#endif
ZEND_INI_END()
zend.enable_gc對(duì)應(yīng)的操作函數(shù)為ZEND_INI_MH(OnUpdateGCEnabled),如果開啟了垃圾回收機(jī)制, 即GC_G(gc_enabled)為真,則會(huì)調(diào)用gc_init函數(shù)執(zhí)行垃圾回收機(jī)制的初始化操作。 gc_init函數(shù)在zend/zend_gc.c 121行,此函數(shù)會(huì)判斷是否開啟垃圾回收機(jī)制, 如果開啟,則初始化整個(gè)機(jī)制,即直接調(diào)用malloc給整個(gè)緩存列表分配10000個(gè)gc_root_buffer內(nèi)存空間。 這里的10000是硬編碼在代碼中的,以宏GC_ROOT_BUFFER_MAX_ENTRIES存在,如果需要修改這個(gè)值,則需要修改源碼,重新編譯PHP。 gc_init函數(shù)在預(yù)分配內(nèi)存后調(diào)用gc_reset函數(shù)重置整個(gè)機(jī)制用到的一些全局變量,如設(shè)置gc運(yùn)行的次數(shù)統(tǒng)計(jì)(gc_runs)和gc中垃圾的個(gè)數(shù)(collected)為0, 設(shè)置雙向鏈表頭結(jié)點(diǎn)的上一個(gè)結(jié)點(diǎn)和下一個(gè)結(jié)點(diǎn)指向自己等。除了這種提的一些用于垃圾回收機(jī)制的全局變量,還有其它一些使用較多的變量,部分說明如下:
復(fù)制代碼 代碼如下:
typedef struct _zend_gc_globals {
zend_bool gc_enabled; /* 是否開啟垃圾收集機(jī)制 */
zend_bool gc_active; /* 是否正在進(jìn)行 */
gc_root_buffer *buf; /* 預(yù)分配的緩沖區(qū)數(shù)組,默認(rèn)為10000(preallocated arrays of buffers) */
gc_root_buffer roots; /* 列表的根結(jié)點(diǎn)(list of possible roots of cycles) */
gc_root_buffer *unused; /* 沒有使用過的緩沖區(qū)列表(list of unused buffers) */
gc_root_buffer *first_unused; /* 指向第一個(gè)沒有使用過的緩沖區(qū)結(jié)點(diǎn)(pointer to first unused buffer) */
gc_root_buffer *last_unused; /* 指向最后一個(gè)沒有使用過的緩沖區(qū)結(jié)點(diǎn),此處為標(biāo)記結(jié)束用(pointer to last unused buffer) */
zval_gc_info *zval_to_free; /* 將要釋放的zval變量的臨時(shí)列表(temporaryt list of zvals to free) */
zval_gc_info *free_list; /* 臨時(shí)變量,需要釋放的列表開頭 */
zval_gc_info *next_to_free; /* 臨時(shí)變量,下一個(gè)將要釋放的變量位置*/
zend_uint gc_runs; /* gc運(yùn)行的次數(shù)統(tǒng)計(jì) */
zend_uint collected; /* gc中垃圾的個(gè)數(shù) */
// 省略...
}
當(dāng)我們使用一個(gè)unset操作想清除這個(gè)變量所占的內(nèi)存時(shí)(可能只是引用計(jì)數(shù)減一),會(huì)從當(dāng)前符號(hào)的哈希表中刪除變量名對(duì)應(yīng)的項(xiàng), 在所有的操作執(zhí)行完后,并對(duì)從符號(hào)表中刪除的項(xiàng)調(diào)用一個(gè)析構(gòu)函數(shù),臨時(shí)變量會(huì)調(diào)用zval_dtor,一般的變量會(huì)調(diào)用zval_ptr_dtor。
當(dāng)然我們無法在PHP的函數(shù)集中找到unset函數(shù),因?yàn)樗且环N語言結(jié)構(gòu)。 其對(duì)應(yīng)的中間代碼為ZEND_UNSET,在Zend/zend_vm_execute.h文件中你可以找到與它相關(guān)的實(shí)現(xiàn)。
zval_ptr_dtor并不是一個(gè)函數(shù),只是一個(gè)長(zhǎng)得有點(diǎn)像函數(shù)的宏。 在Zend/zend_variables.h文件中,這個(gè)宏指向函數(shù)_zval_ptr_dtor。 在Zend/zend_execute_API.c 424行,函數(shù)相關(guān)代碼如下:
復(fù)制代碼 代碼如下:
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);
}
}
/* }}} */
從代碼我們可以很清晰的看出這個(gè)zval的析構(gòu)過程,關(guān)于引用計(jì)數(shù)字段做了以下兩個(gè)操作:
如果變量的引用計(jì)數(shù)為1,即減一后引用計(jì)數(shù)為0,直接清除變量。如果當(dāng)前變量如果被緩存,則需要清除緩存如果變量的引用計(jì)數(shù)大于1,即減一后引用計(jì)數(shù)大于0,則將變量放入垃圾列表。如果變更存在引用,則去掉其引用。
將變量放入垃圾列表的操作是GC_ZVAL_CHECK_POSSIBLE_ROOT,這也是一個(gè)宏,其對(duì)應(yīng)函數(shù)gc_zval_check_possible_root, 但是此函數(shù)僅對(duì)數(shù)組和對(duì)象執(zhí)行垃圾回收操作。對(duì)于數(shù)組和對(duì)象變量,它會(huì)調(diào)用gc_zval_possible_root函數(shù)。
復(fù)制代碼 代碼如下:
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函數(shù)僅對(duì)數(shù)組和對(duì)象執(zhí)行垃圾回收操作,然而在gc_zval_possible_root函數(shù)中, 針對(duì)對(duì)象類型的變量會(huì)去調(diào)用GC_ZOBJ_CHECK_POSSIBLE_ROOT宏。而對(duì)于其它的可用于垃圾回收的機(jī)制的變量類型其調(diào)用過程如下:
檢查zval結(jié)點(diǎn)信息是否已經(jīng)放入到結(jié)點(diǎn)緩沖區(qū),如果已經(jīng)放入到結(jié)點(diǎn)緩沖區(qū),則直接返回,這樣可以優(yōu)化其性能。 然后處理對(duì)象結(jié)點(diǎn),直接返回,不再執(zhí)行后面的操作判斷結(jié)點(diǎn)是否已經(jīng)被標(biāo)記為紫色,如果為紫色則不再添加到結(jié)點(diǎn)緩沖區(qū),此處在于保證一個(gè)結(jié)點(diǎn)只執(zhí)行一次添加到緩沖區(qū)的操作。
將結(jié)點(diǎn)的顏色標(biāo)記為紫色,表示此結(jié)點(diǎn)已經(jīng)添加到緩沖區(qū),下次不用再做添加
找出新的結(jié)點(diǎn)的位置,如果緩沖區(qū)滿了,則執(zhí)行垃圾回收操作。
將新的結(jié)點(diǎn)添加到緩沖區(qū)所在的雙向鏈表。
在gc_zval_possible_root函數(shù)中,當(dāng)緩沖區(qū)滿時(shí),程序調(diào)用gc_collect_cycles函數(shù),執(zhí)行垃圾回收操作。 其中最關(guān)鍵的幾步就是:
第628行 此處為其官方文檔中算法的步驟 B ,算法使用深度優(yōu)先搜索查找所有可能的根,找到后將每個(gè)變量容器中的引用計(jì)數(shù)減1, 為確保不會(huì)對(duì)同一個(gè)變量容器減兩次“1”,用灰色標(biāo)記已減過1的。
第629行 這是算法的步驟 C ,算法再一次對(duì)每個(gè)根節(jié)點(diǎn)使用深度優(yōu)先搜索,檢查每個(gè)變量容器的引用計(jì)數(shù)。 如果引用計(jì)數(shù)是 0 ,變量容器用白色來標(biāo)記。如果引用次數(shù)大于0,則恢復(fù)在這個(gè)點(diǎn)上使用深度優(yōu)先搜索而將引用計(jì)數(shù)減1的操作(即引用計(jì)數(shù)加1), 然后將它們重新用黑色標(biāo)記。
第630行 算法的最后一步 D ,算法遍歷根緩沖區(qū)以從那里刪除變量容器根(zval roots), 同時(shí),檢查是否有在上一步中被白色標(biāo)記的變量容器。每個(gè)被白色標(biāo)記的變量容器都被清除。 在[gc_collect_cycles() -> gc_collect_roots() -> zval_collect_white() ]中我們可以看到, 對(duì)于白色標(biāo)記的結(jié)點(diǎn)會(huì)被添加到全局變量zval_to_free列表中。此列表在后面的操作中有用到。
PHP的垃圾回收機(jī)制在執(zhí)行過程中以四種顏色標(biāo)記狀態(tài)。
GC_WHITE 白色表示垃圾
GC_PURPLE 紫色表示已放入緩沖區(qū)
GC_GREY 灰色表示已經(jīng)進(jìn)行了一次refcount的減一操作
GC_BLACK 黑色是默認(rèn)顏色,正常
相關(guān)的標(biāo)記以及操作代碼如下:
復(fù)制代碼 代碼如下:
#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))
以上的這種以位來標(biāo)記狀態(tài)的方式在PHP的源碼中使用頻率較高,如內(nèi)存管理等都有用到, 這是一種比較高效及節(jié)省的方案。但是在我們做數(shù)據(jù)庫設(shè)計(jì)時(shí)可能對(duì)于字段不能使用這種方式, 應(yīng)該是以一種更加直觀,更加具有可讀性的方式實(shí)現(xiàn)。
相關(guān)文章
phpmailer簡(jiǎn)單發(fā)送郵件的方法(附phpmailer源碼下載)
這篇文章主要介紹了phpmailer簡(jiǎn)單發(fā)送郵件的方法,提供了phpmailer的源碼與相應(yīng)的設(shè)置、使用方法,需要的朋友可以參考下2016-06-06php Notice: Undefined index 錯(cuò)誤提示解決方法
字面意思就是未定義的索引,一般情況下是因?yàn)槌绦蜷_發(fā)作者判斷不嚴(yán)謹(jǐn)導(dǎo)致。一般不會(huì)影響程序的運(yùn)行,具體的解決方法可以參考下。2010-08-08PHP實(shí)現(xiàn)自動(dòng)識(shí)別原編碼并對(duì)字符串進(jìn)行編碼轉(zhuǎn)換的方法
這篇文章主要介紹了PHP實(shí)現(xiàn)自動(dòng)識(shí)別原編碼并對(duì)字符串進(jìn)行編碼轉(zhuǎn)換的方法,涉及php針對(duì)編碼的識(shí)別、轉(zhuǎn)換及數(shù)組的遍歷等相關(guān)操作技巧,需要的朋友可以參考下2016-07-07php進(jìn)行支付寶開發(fā)中return_url和notify_url的區(qū)別分析
這篇文章主要介紹了php進(jìn)行支付寶開發(fā)中return_url和notify_url的區(qū)別,較為詳細(xì)的分析了return_url和notify_url的區(qū)別與用法,需要的朋友可以參考下2014-12-12Thinkphp框架開發(fā)移動(dòng)端接口(2)
這篇文章主要介紹了thinkphp框架開發(fā)移動(dòng)端接口的第2種方法,實(shí)現(xiàn)移動(dòng)端訪問自動(dòng)切換移動(dòng)主題模板,從而實(shí)現(xiàn)偽app訪問,感興趣的小伙伴們可以參考一下2016-08-08PHP中addslashes與mysql_escape_string的區(qū)別分析
這篇文章主要介紹了PHP中addslashes與mysql_escape_string的區(qū)別,簡(jiǎn)單分析了addslashes與mysql_escape_string在使用過程中的區(qū)別,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-04-04