摘要:以上詳細的講解請看源碼學(xué)習(xí)引用那么如何解決循環(huán)引用帶來的內(nèi)存泄漏問題呢我們的垃圾回收就要派上用場了。接下來判斷如果垃圾回收器已經(jīng)運行,那么本次就不再執(zhí)行了。
baiyan
全部視頻:https://segmentfault.com/a/11...
垃圾回收觸發(fā)條件我們知道,在PHP中,如果一個變量的引用計數(shù)減少到0(沒有任何地方在使用這個變量),它所占用的內(nèi)存就會被PHP虛擬機自動回收,并不會被當(dāng)做垃圾。垃圾回收的觸發(fā)條件是當(dāng)一個變量的引用計數(shù)的值減少1之后,仍不為0(還有某個地方在使用這個變量),才有可能是垃圾。需要讓我們?nèi)斯とζ溥M行進一步的檢驗,看它是否真的是垃圾,然后再做后續(xù)的操作。一個典型的例子就是在我們使用數(shù)組與對象的過程中可能存在的循環(huán)引用問題。它會讓某個變量自己引用自己??聪旅嬉粋€例子:
time()]; $a[] = &$a; //循環(huán)引用 unset($a);
我們可以知道,unset($a)之后,$a的type類型變成了0(IS_UNDEF),同時其指向的zend_reference結(jié)構(gòu)體的refcount變?yōu)榱?(因為$a數(shù)組中的元素仍然在引用它),我們畫圖來表示一下現(xiàn)在的內(nèi)存情況:
那么問題出現(xiàn)了,$a是unset掉了,但是由于原始的zend_array中的元素仍然在指向仍然在指向zend_reference結(jié)構(gòu)體,所以zend_reference的refcount是1,而并非是預(yù)期的0。這樣一來,這兩個zend_reference與zend_array結(jié)構(gòu)在unset($a)之后,仍然存在于內(nèi)存之中,如果對此不作任何處理,就會造成內(nèi)存泄漏。
以上詳細的講解請看:【PHP源碼學(xué)習(xí)】2019-03-19 PHP引用
那么如何解決循環(huán)引用帶來的內(nèi)存泄漏問題呢?我們的垃圾回收就要派上用場了。
在PHP7中,垃圾回收分為垃圾回收器和垃圾回收算法兩大部分
在這篇筆記中只講解第一部分:垃圾回收器
垃圾回收器在PHP7中,如果檢測到refcount減1后仍大于0的變量,會首先把它放入一個雙向鏈表中,它就是我們的垃圾回收器。這個垃圾回收器相當(dāng)于一個緩沖區(qū)的作用,待緩沖區(qū)滿了之后,等待垃圾回收算法進行后續(xù)的標記與清除操作。
垃圾回收算法的啟動時機并不是簡單的有一個疑似垃圾到來,就要運行一次,而是待緩沖區(qū)存滿了之后(規(guī)定10001個存儲單元),然后垃圾回收算法才會啟動,對緩沖區(qū)中的疑似垃圾進行最終的標記和清除。這個垃圾回收器緩沖區(qū)的作用就是減少垃圾回收算法運行的頻率,減少對操作系統(tǒng)資源的占用以及對正在運行的服務(wù)端代碼的影響,下面我們通過代碼來詳細講解。
垃圾回收器存儲結(jié)構(gòu)垃圾回收器的結(jié)構(gòu)如下:
typedef struct _gc_root_buffer { zend_refcounted *ref; struct _gc_root_buffer *next; //雙向鏈表,指向下一個緩沖區(qū)單元 struct _gc_root_buffer *prev; //雙向鏈表,指向上一個緩沖區(qū)單元 uint32_t refcount; } gc_root_buffer;
垃圾回收器是一個雙向鏈表,那么如何維護這個雙向鏈表首尾指針的信息,還有緩沖區(qū)的使用情況等額外信息呢,現(xiàn)在就需要使用我們的全局變量zend_gc_globals了:
typedef struct _zend_gc_globals { zend_bool gc_enabled; //是否啟用gc zend_bool gc_active; //當(dāng)前是否正在運行g(shù)c zend_bool gc_full; //緩沖區(qū)是否滿了 gc_root_buffer *buf; /*指向緩沖區(qū)頭部 */ gc_root_buffer roots; /*當(dāng)前處理的垃圾緩沖區(qū)單元,注意這里不是指針*/ gc_root_buffer *unused; /*指向未使用的緩沖區(qū)單元鏈表開頭(用于串聯(lián)緩沖區(qū)碎片)*/ gc_root_buffer *first_unused; /*指向第一個未使用的緩沖區(qū)單元*/ gc_root_buffer *last_unused; /*指向最后一個未使用的緩沖區(qū)單元 */ gc_root_buffer to_free; gc_root_buffer *next_to_free; ... } zend_gc_globals;垃圾回收器初始化
那么現(xiàn)在,我們需要為垃圾回收器分配內(nèi)存空間,以存儲接下來可能到來的可疑垃圾,我們通過gc_init()函數(shù)實現(xiàn)空間的分配:
ZEND_API void gc_init(void) { if (GC_G(buf) == NULL && GC_G(gc_enabled)) { GC_G(buf) = (gc_root_buffer*) malloc(sizeof(gc_root_buffer) * GC_ROOT_BUFFER_MAX_ENTRIES); GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES]; gc_reset(); } }
GC_G這個宏是取得以上zend_gc_globals結(jié)構(gòu)體中的變量。我們現(xiàn)在還沒有生成緩沖區(qū),所以進入這個if分支。通過系統(tǒng)調(diào)用malloc分配一塊內(nèi)存,這個內(nèi)存的大小是單個緩沖區(qū)結(jié)構(gòu)體的大小 * 10001:
#define GC_ROOT_BUFFER_MAX_ENTRIES 10001
那么現(xiàn)在我們得到了大小為10001的緩沖區(qū)(第1個單元不用),并把指針的步長置為gc_root_buffer類型,隨后將它的last_unused指針指向緩沖區(qū)的末尾,然后通過gc_reset()做一些初始化操作:
ZEND_API void gc_reset(void) { GC_G(gc_runs) = 0; GC_G(collected) = 0; GC_G(gc_full) = 0; ... GC_G(roots).next = &GC_G(roots); GC_G(roots).prev = &GC_G(roots); GC_G(to_free).next = &GC_G(to_free); GC_G(to_free).prev = &GC_G(to_free); if (GC_G(buf)) { //由于我們之前分配了緩沖區(qū),進這里 GC_G(unused) = NULL; //沒有緩沖區(qū)碎片,置指針為NULL GC_G(first_unused) = GC_G(buf) + 1; //將指向第一個未使用空間的指針往后挪1個單元的長度 } else { GC_G(unused) = NULL; GC_G(first_unused) = NULL; GC_G(last_unused) = NULL; } GC_G(additional_buffer) = NULL; }
根據(jù)這個函數(shù)中的內(nèi)容,我們可以畫出當(dāng)前的內(nèi)存結(jié)構(gòu)圖:
將疑似垃圾存入垃圾回收器這樣一來,我們垃圾回收器緩沖區(qū)就初始化完畢了,現(xiàn)在等著zend虛擬機收集可能會是垃圾的變量,存入這些緩沖區(qū)中,這步操作通過gc_possible_root(zend_refcounted *ref)函數(shù)完成:
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; } ZEND_ASSERT(GC_TYPE(ref) == IS_ARRAY || GC_TYPE(ref) == IS_OBJECT); ZEND_ASSERT(EXPECTED(GC_REF_GET_COLOR(ref) == GC_BLACK)); ZEND_ASSERT(!GC_ADDRESS(GC_INFO(ref))); GC_BENCH_INC(zval_possible_root); 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)) { return; } GC_REFCOUNT(ref)++; gc_collect_cycles(); GC_REFCOUNT(ref)--; if (UNEXPECTED(GC_REFCOUNT(ref)) == 0) { zval_dtor_func(ref); return; } if (UNEXPECTED(GC_INFO(ref))) { return; } newRoot = GC_G(unused); if (!newRoot) { #if ZEND_GC_DEBUG if (!GC_G(gc_full)) { fprintf(stderr, "GC: no space to record new root candidate "); GC_G(gc_full) = 1; } #endif return; } GC_G(unused) = newRoot->prev; } GC_TRACE_SET_COLOR(ref, GC_PURPLE); GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE; newRoot->ref = ref; newRoot->next = GC_G(roots).next; newRoot->prev = &GC_G(roots); GC_G(roots).next->prev = newRoot; GC_G(roots).next = newRoot; GC_BENCH_INC(zval_buffered); GC_BENCH_INC(root_buf_length); GC_BENCH_PEAK(root_buf_peak, root_buf_length); }
代碼有點長不要緊,我們逐行分析。首先又聲明了一個指向緩沖區(qū)的指針newRoot。接下來判斷如果垃圾回收器已經(jīng)運行,那么本次就不再執(zhí)行了。然后將zend_gc_globals全局變量上的unused指針字段賦值給newRoot指針,然而unused指針為NULL(因為沒有緩沖區(qū)碎片),所以newRoot此時也為NULL。故接下來進入else if分支:
newRoot = GC_G(first_unused); GC_G(first_unused)++;
首先將newRoot指向第一個未使用的緩沖區(qū)單元,所以下一行需要將第一個未使用的緩沖區(qū)單元往后挪一個單元,方便下一次的使用,很好理解,跳過這個長長的else分支往下繼續(xù)執(zhí)行:
GC_TRACE_SET_COLOR(ref, GC_PURPLE); GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE; newRoot->ref = ref; newRoot->next = GC_G(roots).next; newRoot->prev = &GC_G(roots); GC_G(roots).next->prev = newRoot; GC_G(roots).next = newRoot;
第一行GC_TRACE這個宏用來打印相關(guān)DEBUG信息,我們略過這一行。
第二行執(zhí)行GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE;我們看到這里有一個GC_PURPLE,也就是顏色的概念。在PHP垃圾回收中,用到了4種顏色:
#define GC_BLACK 0x0000 #define GC_WHITE 0x8000 #define GC_GREY 0x4000 #define GC_PURPLE 0xc000
源碼中對它們的解釋如下:
* BLACK (GC_BLACK) - In use or free. * GREY (GC_GREY) - Possible member of cycle. * WHITE (GC_WHITE) - Member of garbage cycle. * PURPLE (GC_PURPLE) - Possible root of cycle.
這里我們先不對每一種顏色做詳細解釋。我們用(newRoot - GC_G(buf)) | GC_PURPLE的意思是:newRoot - GC_G(buf)(緩沖區(qū)起始地址)代表當(dāng)前使用的緩沖區(qū)的偏移量,再與0xc000做或運算,結(jié)果拼裝到變量的gc_info字段中,這個字段是一個uint16類型,所以可以利用前2位把它標記成紫色,同時利用后14位存儲偏移量。最終字段按位拆開的情況如圖:
第三行:將當(dāng)前引用賦值到當(dāng)前緩沖區(qū)中
接下來是雙向鏈表的指針操作:
newRoot->next = GC_G(roots).next; newRoot->prev = &GC_G(roots); GC_G(roots).next->prev = newRoot; GC_G(roots).next = newRoot;
其目的是將當(dāng)前緩沖區(qū)的prev和next指針指向全局變量中的root字段,同時將全局變量中的root字段的prev與next指針指向當(dāng)前使用的緩沖區(qū)。
至此,我們就可以將所有疑似垃圾的變量都放到緩沖區(qū)中,一直存下去,待存滿緩沖區(qū)10000個存儲單元之后,垃圾回收算法就會啟動,對緩沖區(qū)中的所有疑似垃圾進行標記與清除,垃圾回收算法的過程會在下一篇筆記進行講解。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/31662.html
摘要:此文用于匯總跟隨陳雷老師及團隊的視頻,學(xué)習(xí)源碼過程中的思考整理與心得體會,此文會不斷更新視頻傳送門每日學(xué)習(xí)記錄使用錄像設(shè)備記錄每天的學(xué)習(xí)源碼學(xué)習(xí)源碼學(xué)習(xí)內(nèi)存管理筆記源碼學(xué)習(xí)內(nèi)存管理筆記源碼學(xué)習(xí)內(nèi)存管理筆記源碼學(xué)習(xí)基本變量筆記 此文用于匯總跟隨陳雷老師及團隊的視頻,學(xué)習(xí)源碼過程中的思考、整理與心得體會,此文會不斷更新 視頻傳送門:【每日學(xué)習(xí)記錄】使用錄像設(shè)備記錄每天的學(xué)習(xí) PHP7...
摘要:在這里使用學(xué)而思網(wǎng)校的錄像設(shè)備,記錄每天學(xué)習(xí)的內(nèi)容執(zhí)行潘森執(zhí)行潘森執(zhí)行潘森趙俊峰紅黑樹景羅紅黑樹景羅配置三叉樹田志澤新建模塊馬運運配置田志澤田志澤田志澤李樂田志澤田志澤文件系統(tǒng) 在這里使用學(xué)而思網(wǎng)校的錄像設(shè)備,記錄每天學(xué)習(xí)的內(nèi)容: 2019-07-15 ~ 2019-07-19 07-18 nginx http 執(zhí)行 by 潘森 07-17 nginx http 執(zhí)行 by 潘森 07...
閱讀 3022·2021-10-27 14:15
閱讀 3015·2021-09-07 10:18
閱讀 1332·2019-08-30 15:53
閱讀 1584·2019-08-26 18:18
閱讀 3385·2019-08-26 12:15
閱讀 3468·2019-08-26 10:43
閱讀 662·2019-08-23 16:43
閱讀 2218·2019-08-23 15:27