【PHP7源碼學習】2019-04-01 PHP垃圾回收1

baiyanphp

所有視頻:https://segmentfault.com/a/11...算法

垃圾回收觸發條件

  • 咱們知道,在PHP中,若是一個變量的引用計數減小到0(沒有任何地方在使用這個變量),它所佔用的內存就會被PHP虛擬機自動回收,並不會被當作垃圾。垃圾回收的觸發條件是當一個變量的引用計數的值減小1以後,仍不爲0(還有某個地方在使用這個變量),才有可能是垃圾。須要讓咱們人工去對其進行進一步的檢驗,看它是否真的是垃圾,而後再作後續的操做。一個典型的例子就是在咱們使用數組對象的過程當中可能存在的循環引用問題。它會讓某個變量本身引用本身。看下面一個例子:
<?php
$a = ['time' => 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\n");
                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個存儲單元以後,垃圾回收算法就會啓動,對緩衝區中的全部疑似垃圾進行標記與清除,垃圾回收算法的過程會在下一篇筆記進行講解。
相關文章
相關標籤/搜索