摘要:以上詳細的講解請看源碼學習引用那么如何解決循環引用帶來的內存泄漏問題呢我們的垃圾回收就要派上用場了。接下來判斷如果垃圾回收器已經運行,那么本次就不再執行了。
baiyan
全部視頻:https://segmentfault.com/a/11...
垃圾回收觸發條件我們知道,在PHP中,如果一個變量的引用計數減少到0(沒有任何地方在使用這個變量),它所占用的內存就會被PHP虛擬機自動回收,并不會被當做垃圾。垃圾回收的觸發條件是當一個變量的引用計數的值減少1之后,仍不為0(還有某個地方在使用這個變量),才有可能是垃圾。需要讓我們人工去對其進行進一步的檢驗,看它是否真的是垃圾,然后再做后續的操作。一個典型的例子就是在我們使用數組與對象的過程中可能存在的循環引用問題。它會讓某個變量自己引用自己。看下面一個例子:
time()]; $a[] = &$a; //循環引用 unset($a);
我們可以知道,unset($a)之后,$a的type類型變成了0(IS_UNDEF),同時其指向的zend_reference結構體的refcount變為了1(因為$a數組中的元素仍然在引用它),我們畫圖來表示一下現在的內存情況:
那么問題出現了,$a是unset掉了,但是由于原始的zend_array中的元素仍然在指向仍然在指向zend_reference結構體,所以zend_reference的refcount是1,而并非是預期的0。這樣一來,這兩個zend_reference與zend_array結構在unset($a)之后,仍然存在于內存之中,如果對此不作任何處理,就會造成內存泄漏。
以上詳細的講解請看:【PHP源碼學習】2019-03-19 PHP引用
那么如何解決循環引用帶來的內存泄漏問題呢?我們的垃圾回收就要派上用場了。
在PHP7中,垃圾回收分為垃圾回收器和垃圾回收算法兩大部分
在這篇筆記中只講解第一部分:垃圾回收器
垃圾回收器在PHP7中,如果檢測到refcount減1后仍大于0的變量,會首先把它放入一個雙向鏈表中,它就是我們的垃圾回收器。這個垃圾回收器相當于一個緩沖區的作用,待緩沖區滿了之后,等待垃圾回收算法進行后續的標記與清除操作。
垃圾回收算法的啟動時機并不是簡單的有一個疑似垃圾到來,就要運行一次,而是待緩沖區存滿了之后(規定10001個存儲單元),然后垃圾回收算法才會啟動,對緩沖區中的疑似垃圾進行最終的標記和清除。這個垃圾回收器緩沖區的作用就是減少垃圾回收算法運行的頻率,減少對操作系統資源的占用以及對正在運行的服務端代碼的影響,下面我們通過代碼來詳細講解。
垃圾回收器存儲結構垃圾回收器的結構如下:
typedef struct _gc_root_buffer { zend_refcounted *ref; struct _gc_root_buffer *next; //雙向鏈表,指向下一個緩沖區單元 struct _gc_root_buffer *prev; //雙向鏈表,指向上一個緩沖區單元 uint32_t refcount; } gc_root_buffer;
垃圾回收器是一個雙向鏈表,那么如何維護這個雙向鏈表首尾指針的信息,還有緩沖區的使用情況等額外信息呢,現在就需要使用我們的全局變量zend_gc_globals了:
typedef struct _zend_gc_globals { zend_bool gc_enabled; //是否啟用gc zend_bool gc_active; //當前是否正在運行gc zend_bool gc_full; //緩沖區是否滿了 gc_root_buffer *buf; /*指向緩沖區頭部 */ gc_root_buffer roots; /*當前處理的垃圾緩沖區單元,注意這里不是指針*/ gc_root_buffer *unused; /*指向未使用的緩沖區單元鏈表開頭(用于串聯緩沖區碎片)*/ gc_root_buffer *first_unused; /*指向第一個未使用的緩沖區單元*/ gc_root_buffer *last_unused; /*指向最后一個未使用的緩沖區單元 */ gc_root_buffer to_free; gc_root_buffer *next_to_free; ... } zend_gc_globals;垃圾回收器初始化
那么現在,我們需要為垃圾回收器分配內存空間,以存儲接下來可能到來的可疑垃圾,我們通過gc_init()函數實現空間的分配:
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結構體中的變量。我們現在還沒有生成緩沖區,所以進入這個if分支。通過系統調用malloc分配一塊內存,這個內存的大小是單個緩沖區結構體的大小 * 10001:
#define GC_ROOT_BUFFER_MAX_ENTRIES 10001
那么現在我們得到了大小為10001的緩沖區(第1個單元不用),并把指針的步長置為gc_root_buffer類型,隨后將它的last_unused指針指向緩沖區的末尾,然后通過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)) { //由于我們之前分配了緩沖區,進這里 GC_G(unused) = NULL; //沒有緩沖區碎片,置指針為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; }
根據這個函數中的內容,我們可以畫出當前的內存結構圖:
將疑似垃圾存入垃圾回收器這樣一來,我們垃圾回收器緩沖區就初始化完畢了,現在等著zend虛擬機收集可能會是垃圾的變量,存入這些緩沖區中,這步操作通過gc_possible_root(zend_refcounted *ref)函數完成:
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); }
代碼有點長不要緊,我們逐行分析。首先又聲明了一個指向緩沖區的指針newRoot。接下來判斷如果垃圾回收器已經運行,那么本次就不再執行了。然后將zend_gc_globals全局變量上的unused指針字段賦值給newRoot指針,然而unused指針為NULL(因為沒有緩沖區碎片),所以newRoot此時也為NULL。故接下來進入else if分支:
newRoot = GC_G(first_unused); GC_G(first_unused)++;
首先將newRoot指向第一個未使用的緩沖區單元,所以下一行需要將第一個未使用的緩沖區單元往后挪一個單元,方便下一次的使用,很好理解,跳過這個長長的else分支往下繼續執行:
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這個宏用來打印相關DEBUG信息,我們略過這一行。
第二行執行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)(緩沖區起始地址)代表當前使用的緩沖區的偏移量,再與0xc000做或運算,結果拼裝到變量的gc_info字段中,這個字段是一個uint16類型,所以可以利用前2位把它標記成紫色,同時利用后14位存儲偏移量。最終字段按位拆開的情況如圖:
第三行:將當前引用賦值到當前緩沖區中
接下來是雙向鏈表的指針操作:
newRoot->next = GC_G(roots).next; newRoot->prev = &GC_G(roots); GC_G(roots).next->prev = newRoot; GC_G(roots).next = newRoot;
其目的是將當前緩沖區的prev和next指針指向全局變量中的root字段,同時將全局變量中的root字段的prev與next指針指向當前使用的緩沖區。
至此,我們就可以將所有疑似垃圾的變量都放到緩沖區中,一直存下去,待存滿緩沖區10000個存儲單元之后,垃圾回收算法就會啟動,對緩沖區中的所有疑似垃圾進行標記與清除,垃圾回收算法的過程會在下一篇筆記進行講解。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/31662.html
摘要:此文用于匯總跟隨陳雷老師及團隊的視頻,學習源碼過程中的思考整理與心得體會,此文會不斷更新視頻傳送門每日學習記錄使用錄像設備記錄每天的學習源碼學習源碼學習內存管理筆記源碼學習內存管理筆記源碼學習內存管理筆記源碼學習基本變量筆記 此文用于匯總跟隨陳雷老師及團隊的視頻,學習源碼過程中的思考、整理與心得體會,此文會不斷更新 視頻傳送門:【每日學習記錄】使用錄像設備記錄每天的學習 PHP7...
摘要:在這里使用學而思網校的錄像設備,記錄每天學習的內容執行潘森執行潘森執行潘森趙俊峰紅黑樹景羅紅黑樹景羅配置三叉樹田志澤新建模塊馬運運配置田志澤田志澤田志澤李樂田志澤田志澤文件系統 在這里使用學而思網校的錄像設備,記錄每天學習的內容: 2019-07-15 ~ 2019-07-19 07-18 nginx http 執行 by 潘森 07-17 nginx http 執行 by 潘森 07...
閱讀 3016·2021-10-27 14:15
閱讀 3009·2021-09-07 10:18
閱讀 1326·2019-08-30 15:53
閱讀 1580·2019-08-26 18:18
閱讀 3380·2019-08-26 12:15
閱讀 3466·2019-08-26 10:43
閱讀 659·2019-08-23 16:43
閱讀 2213·2019-08-23 15:27