-
- struct _zval_struct {
- /* Variable information */
- zvalue_value value; /* value */zend_uchar is_ref__gc;
- };
-
複製程式碼
與PHP5.3之前的版本
與PHP5.3之前的版本相比,引用計數之前的版本refcount和是否引用欄位is_ref都在其後面添加了__gc以用於新的垃圾回收機制。 在PHP的源碼風格中,大量的宏是一個非常鮮明的特徵。這些宏相當於一個介面層,它屏蔽了介面層以下的一些底層實現,如, ALLOC_ZVAL宏,這個宏在PHP5.3之前是直接呼叫PHP的記憶體管理分配函數emalloc分配內存,所分配的記憶體大小由變數的類型等大小決定。 在引入垃圾回收機制後,ALLOC_ZVAL宏直接採用新的垃圾回收單元結構,所分配的大小都是一樣的,全部是zval_gc_info結構體所佔記憶體大小, 且在分配記憶體後,初始化這個結構體的垃圾回收機制。
-
-
- /* The following macroses override macroses from zend_alloc.h */
- #undef ALLOC_Z#
- dodefine ALL_🎜>#undef ALLOC_Z#
- dodefine> {
- (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等宏在新的的宏中,關鍵性的改變是對所分配記憶體大小和分配內容的改變,在以前純粹的記憶體分配中加入了垃圾收集機制的內容, 所有的內容都包括在zval_gc_info結構體中:
-
-
-
- typedef struct _zval_gc_info {
- zval z;
- union {
- gc_root_bufferf *buffered >} u;
- } zval_gc_info;
複製程式碼
對於任何一個ZVAL容器儲存的變量,分配了一個zval結構,這個結構確保其和以zval變數分配的記憶體的開始對齊, 從而在zval_gc_info類型指標的強制轉換時,其可以作為zval使用。在zval字段後面有一個聯合體:u。 u包含gc_root_buffer結構的buffered欄位和zval_gc_info結構的next欄位。 這兩個字段一個是表示垃圾收集機制緩存的根結點,一個是zval_gc_info列表的下一個結點, 垃圾收集機制緩存的結點無論是作為根結點,還是列表結點,都可以在這裡體現。 ALLOC_ZVAL在分配了記憶體後會呼叫GC_ZVAL_INIT用來初始化替代了zval的zval_gc_info, 它會把zval_gc_info中的成員u的buffered欄位設為NULL,此欄位僅在將其放入垃圾回收緩衝區時才會有值,否則會一直是NULL。 由於PHP中所有的變數都是以zval變數的形式存在,這裡以zval_gc_info取代zval,從而成功實現垃圾收集機制在原有系統中的整合。
PHP的垃圾回收機制在PHP5.3中預設為開啟,但我們可以透過設定檔直接設定為停用,其對應的設定欄位為:zend.enable_gc。 在php.ini檔案中預設是沒有這個欄位的,如果我們需要停用此功能,則在php.ini中加入zend.enable_gc=0或zend.enable_gc=off。 除了修改php.ini配置zend.enable_gc,也可以透過呼叫gc_enable()/gc_disable()函數來開啟/關閉垃圾回收機制。 這些函數的呼叫效果與修改配置項來開啟或關閉垃圾回收機制的效果是一樣的。 除了這兩個函數PHP提供了gc_collect_cycles()函數可以在根緩衝區還沒滿時強制執行週期回收。 與垃圾回收機制是否開啟在PHP源碼中有一些相關的操作和欄位。
在zend.c檔案中有如下程式碼:
-
-
-
-
-
- static ZEND_INI_MH(OnUpdateGCEnabled) /* {{{ */
- {
- OnUpdateBool(marghdateBool(mNargh, margh, margh, margh00,001, margh. , 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", I0,000, NALLable, NpALLable_gftd, gALLable_gft, gALLable, gidable, gidable, fidable_dg, nALL,gable_g; obals, 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.enable_gc對應的運算子為ZEND_INI_MH(OnUpdateGCEnabled),若開啟了垃圾回收機制, 即GC_G(gc_enabled)為真,則會呼叫gc_init函式執行垃圾回收機制的初始化運算。 gc_init函數在zend/zend_gc.c 121行,此函數會判斷是否開啟垃圾回收機制, 如果開啟,則初始化整個機制,即直接呼叫malloc給整個快取清單分配10000個gc_root_buffer記憶體空間。 這裡的10000是硬編碼在程式碼中的,以宏GC_ROOT_BUFFER_MAX_ENTRIES存在,如果需要修改這個值,則需要修改原始碼,重新編譯PHP。 gc_init函數在預先分配記憶體後調用gc_reset函數重置整個機制用到的一些全局變量,如設定gc運行的次數統計(gc_runs)和gc中垃圾的個數(collected)為0,設定雙向鍊錶頭結點的上一個結點和下一個結點指向自己等。除了這種提的一些用於垃圾回收機制的全域變量,還有其它一些使用較多的變量,部分說明如下:
-
- typedef struct _zend_gc_globals {
- zend_bool gc_enabled; /* 是否開啟垃圾收集機制/
- /gc_root_buffer *buf; /* 預先分配的緩衝區數組,預設為10000(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; /* 指向第一個沒有使用過的緩衝區結點(pointer to first unused buffer) */
- gc_root_buffer *last_unused; /* 指向最後一個沒有使用過的緩衝區結點,此處為標記結束用(pointer to last unused buffer) */
- zval_gc_info *zval_to_free; /* 將要釋放的zval變數的臨時列表(temporaryt list of zvals to free) */
- zval_gc_info *free_list; /* 臨時變量,需要釋放的列表開頭*/
- zval_gc_info *next_to_free; /* 臨時變量,下一個臨時變量,下一個將要釋放的變數位置*/
- zend_uint gc_runs; /* gc 運行的次數統計*/
- zend_uint collected; /* gc中垃圾的個數*/
- // 省略...
- }
複製程式碼
當我們使用一個unset操作想清除這個變數所佔的記憶體時(可能只是引用計數減一),會從目前符號的在雜湊表中刪除變數名對應的項, 在所有的操作執行完後,並對從符號表中刪除的項呼叫一個析構函數,臨時變數會呼叫zval_dtor,一般的變數會呼叫zval_ptr_dtor。
當然我們無法在PHP的函數集中找到unset函數,因為它是一種語言結構。 其對應的中間程式碼為ZEND_UNSET,在Zend/zend_vm_execute.h檔案中你可以找到與它相關的實作。
zval_ptr_dtor不是函數,只是一個長得有點像函數的宏。 在Zend/zend_variables.h檔中,這個巨集指向函數_zval_ptr_dtor。 在Zend/zend_execute_API.c 424行,函數相關程式碼如下:
-
- ZEND_API void _zval_ptr_dtor(zval **zval_ptr ZEND_FILE_LINE_DC) /* {{{UG/
- { >printf("Reducing refcount for %x (%x): %d->%dn", *zval_ptr, zval_ptr, Z_REFCOUNT_PP(zval_ptr), Z_REFCOUNT_PP(zval_ptr) - 1);
- #endifif (Z_REFCOUNT_PP(zval_ptr);
- if (Z_REFCOUNT_PP(zval_ptr);
- if (Z_REFCOUNT_PP(zval_ptr) == 0) {
- TSRMLS_FETCH();
- if (*zval_ptr != &EG(uninitialized_zval)) {<_gc_redmo>zval_dtor(*zval_ptr);
- efree_rel(*zval_ptr);
- }
- } else {
- TSRMLS_FETCH();
- if (Z_REFCOUNT_PP(zval_ptr) PP(zval1) Z_UNSET_ISREF_PP(zval_ptr);
- }
- GC_ZVAL_CHECK_POSSIBLE_ROOT(*zval_ptr);
- }
- }
- /* }}} *
- }
- }
- /* }}} *
- >
從程式碼我們可以很清楚的看出這個zval的析構過程,關於引用計數欄位做了以下兩個操作:
若變數的引用計數為1,即減一後引用計數為0,直接清除變數。如果目前變數如果被緩存,則需要清除快取如果變數的參考計數大於1,即減一後引用計數大於0,則將變數放入垃圾清單。如果變更存在引用,則去掉其引用。
將變數放入垃圾清單的操作是GC_ZVAL_CHECK_POSSIBLE_ROOT,這也是一個宏,其對應函數gc_zval_check_possible_root, 但是此函數僅對陣列和物件執行垃圾回收操作。對於數組和物件變量,它會呼叫gc_zval_possible_root函數。
-
- ZEND_API void gc_zval_possible_root(zval *zv TSRMLS_DC)
- {
- if (UNEX裝(zv) != NULL &&
- GC_ZVAL_GET_COLOR(zv) == GC_BLACK) &&
- (GC_ZVAL_ADDRESS(zv) GC_Z_ADDRESS(Zre_ADDRESS(z)>
- /* 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_LRP_GET_COLOR_PUID_COL 🎜> if (!GC_ZVAL_ADDRESS(zv)) {
- gc_root_buffer *newRoot = GC_G(unused);
- if (newRoot) {
- GC_G(unused) = newRoot->prevoot) {
- GC_G(unused) = newRoot->prevoot;
- GC_G(unused) = newRoot->prevoot;} (first_unused) != GC_G(last_unused)) {
- newRoot = GC_G(first_unused);
- GC_G(first_unused)++;
- } else {
- if (!dableG) 🎜>GC_ZVAL_SET_BLACK(zv);
- return;
- }
- zv->refcount__gc++;
- gc_collect_cycles(TSRMLS_C);v->fg; );
- if (!newRoot) {
- return;
- }
- GC_ZVAL_SET_PURPLE(zv);
- GC_G(unused) = newRoot->prev;
- }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_PEAK(root_buf_peak, root_buf_length);
- }
- }
- }
-
-
- 複製代碼
-
-
在前面說到gc_zval_check_possible_root函數只對陣列和物件執行垃圾回收操作,然而在gc_zval_possible_root函數中, 針對物件類型的變數會去呼叫GC_ZOBJ_CHECK_POSSIBLE_ROOT巨集。而對於其它的可用於垃圾回收的機制的變數類型其呼叫過程如下:
檢查zval結點資訊是否已經放入到結點緩衝區,如果已經放入到結點緩衝區,則直接返回,這樣可以最佳化其效能。 然後處理物件結點,直接返回,不再執行後面的操作判斷結點是否已經被標記為紫色,如果為紫色則不再添加到結點緩衝區,此處在於保證一個結點只執行一次添加到緩衝區的操作。
將結點的顏色標記為紫色,表示此結點已經加入到緩衝區,下次不用再做添加。
找出新的結點的位置,如果緩衝區滿了,則執行垃圾回收作業。
將新的結點新增至緩衝區所在的雙向鍊錶。
在gc_zval_possible_root函數中,當緩衝區滿時,程式呼叫gc_collect_cycles函數,執行垃圾回收操作。 其中最關鍵的幾步就是:
第628行此處為其官方文件中演算法的步驟B ,演算法使用深度優先搜尋查找所有可能的根,找到後將每個變數容器中的引用計數減1, 為確保不會對同一個變數容器減兩次“1”,用灰色標記已減過1的。
第629行 這是演算法的步驟 C ,演算法再一次對每個根節點使用深度優先搜索,檢查每個變數容器的引用計數。 如果引用計數是 0 ,變數容器用白色來標記。如果引用次數大於0,則恢復在這個點上使用深度優先搜尋而將引用計數減1的操作(即引用計數加1), 然後將它們重新標記為黑色。
第630行 演算法的最後一步 D ,演算法遍歷根緩衝區以從那裡刪除變數容器根(zval roots), 同時,檢查是否有在上一步中被白色標記的變數容器。每個被白色標記的變數容器都被清除。 在[gc_collect_cycles() -> gc_collect_roots() -> zval_collect_white() ]中我們可以看到, 對於白色標記的結點會被加入到全域變數zval_to_free清單中。此列表在後面的操作中有用到。
PHP的垃圾回收機制在執行過程中以四種顏色標示狀態。
GC_WHITE 白色表示垃圾
GC_PURPLE 紫色表示已放入緩衝區
GC_GREY 灰色表示已經進行了一次refcount的減一操作
GC_BLACK 黑色是預設顏色,正常
相關的標記以及操作代碼如下:
-
- #define GC_COLOR 0x03
- #define GC_BLACK 0x00
- #define GC_WHITEITE 0x01 0x03
- #define GC_ADDRESS(v)
- ((gc_root_buffer*)(((zend_uintptr_t)(v)) & ~GC_COLOR))
- #define GC_SET_ADDRESS(v, a) (>#define GC_SET_ADDRESS(v, a) (vvv) (gc_root_buffer*)((((zend_uintptr_t)(v)) & GC_COLOR) | ((zend_uintptr_t)(a))))
- #define GC_GET_COLOR(v)
- (>(uint) &v) GC_COLOR)
- #define GC_SET_COLOR(v, c)
- (v) = ((gc_root_buffer*)(((zend_uintptr_t)(v)) & ~GC_COLOR) | (c)))
- #define🎜>define GC_SET_BLACK(v)
- (v) = ((gc_root_buffer*)(((zend_uintptr_t)(v)) & ~GC_COLOR))
- #define GC_SET_PURPLE(v)
- (vroot = ((gufferc_ )(((zend_uintptr_t)(v)) | GC_PURPLE))
-
-
- 複製程式碼
以上的方式在PHP的原始碼中使用頻率較高,如記憶體管理等都有用到, 這是比較有效率及節省的方案。但是在我們做資料庫設計時可能對於欄位不能使用這種方式, 應該是以一種更直觀,更具有可讀性的方式實現。
|