erlang garbage_collect 原始碼剖析

2020-08-13 15:31:09

兩個詞可以概括整個gc流程:

  • 分代:數據儲存區域分爲年輕堆(the young heap)和年老堆(the old
    heap),年輕堆的數據在經歷過兩輪小回收後還存活的話,會被轉移到年老堆。
  • 複製:gc時會計算新堆的大小並重新申請空間,將舊堆存活數據複製到新堆

回收型別:

  • 小回收(minor_collection):只對年輕堆進行垃圾回收
  • 大回收(major_collection):年輕堆和年老堆都進行垃圾回收

首先我們來看erl的進程結構(erlang_process.h)中,關於記憶體管理的一些欄位:

Eterm* htop;		/* Heap top */  堆頂
Eterm* stop;		/* Stack top */  棧頂
Eterm* heap;		/* Heap start */  堆起始地址
Eterm* hend;		/* Heap end */  堆結束地址

那麼疑問來了,怎麼沒有棧區的起始地址和結束地址?在process的標頭檔案的宏定義中我們可以找到:

#  define HEAP_START(p)     (p)->heap
#  define HEAP_TOP(p)       (p)->htop
#  define HEAP_END(p)       (p)->hend
#  define STACK_START(p)    (p)->hend
#  define STACK_TOP(p)      (p)->stop
#  define STACK_END(p)      (p)->htop

我們的疑問便得到瞭解答: erl進程空間裡堆區和棧區是記憶體中的同一塊區域,區別在堆的空間從下往上增長,棧的空間從上往下增長。

在erl程式啓動時,會呼叫erts_init_gc函數進行gc相關的一些初始化,其主要邏輯是初始化heap_sizes陣列,該陣列的作用是預先計算進程需要申請的記憶體大小。
eg:heap_sizes[9] = 1598, heap_sizes[10] = 2586, 如果進程當前需要空間大小爲2500,那麼系統會申請的空間大小爲2586,而不是2500; 若使用process_flag(min_heap_size, 2500)設定進程的最小堆空間爲2500,實際上系統設定的值是2586

heap_sizes[0] = 12;
heap_sizes[1] = 38;
for(i = 2; i < 23; i++) {
    heap_sizes[i] = heap_sizes[i-1] + heap_sizes[i-2] + 1;
}
for (i = 23; i < ALENGTH(heap_sizes); i++) {
    heap_sizes[i] = heap_sizes[i-1] + heap_sizes[i-1]/5;
}

heap_sizes陣列前23個值按照斐波拉契數列增長,之後按照百分之20的增長率增長。

下面 下麪爲小回收的實現核心分析

static int minor_collection(Process* p, ...)
{
    if (MAX_HEAP_SIZE_GET(p)) {
        判斷是否設定max_heap_size,設定且超出設定,return -2;
        外部呼叫邏輯會對minor_collection的返回值進行判斷,爲-2時會幹掉當前進程
    }
    if (OLD_HEAP(p) == NULL && mature_size != 0) {
        沒有年老堆的話,建立之
    }
    // 判斷是否有年老堆以及年老堆的剩餘空間是否足夠
    if (OLD_HEAP(p) &&
    ((mature_size <= OLD_HEND(p) - OLD_HTOP(p)) &&
     ((BIN_OLD_VHEAP_SZ(p) > BIN_OLD_VHEAP(p))) ) ) {
        stack_size = p->hend - p->stop;  // 使用的棧記憶體
        new_sz = stack_size + size_before; // size_before 爲young_gen_usage函數計算出的 堆使用記憶體 + msg佔用的進程外部記憶體
        new_sz = next_heap_size(p, new_sz, 0); // 從heap_sizes陣列中找到適合的記憶體空間數值
        prev_old_htop = p->old_htop;

        // 申請new_sz大小的記憶體,並轉移數據
        do_minor(p, live_hf_end, (char *) mature, mature_size*sizeof(Eterm), new_sz, objv, nobj); 

        // message_queue_data 設定爲 on_heap時需要把在外部的msg轉移到進程的堆空間中
        if (p->sig_qs.flags & FS_ON_HEAP_MSGQ)
            move_msgs_to_heap(p);

        new_mature = p->old_htop - prev_old_htop;
        // size_after爲gc後剩下的數據大小
        size_after = new_mature;
        size_after += HEAP_TOP(p) - HEAP_START(p) + p->mbuf_sz;
        // 增加記錄的小回收次數
        GEN_GCS(p)++;
        // need_after 小回收後年輕堆中數據佔用的空間
        need_after = ((HEAP_TOP(p) - HEAP_START(p)) + need + stack_size);
        adjust_size = 0;
        // 調整年輕堆的大小, 擴大縮小或保持不變  調整規則從判斷條件就可以梳理出來,此處不多做描述
        if ((HEAP_SIZE(p) > 3000) && (4 * need_after < HEAP_SIZE(p)) &&
            ((HEAP_SIZE(p) > 8000) || (HEAP_SIZE(p) > (OLD_HEND(p) - OLD_HEAP(p))))) {
            Uint wanted = 3 * need_after;
            Uint old_heap_sz = OLD_HEND(p) - OLD_HEAP(p);
            if (wanted*9 < old_heap_sz) {
                Uint new_wanted = old_heap_sz / 8;
                if (new_wanted > wanted) {
                    wanted = new_wanted;
                }
            }
            wanted = wanted < MIN_HEAP_SIZE(p) ? MIN_HEAP_SIZE(p) : next_heap_size(p, wanted, 0);
            if (wanted < HEAP_SIZE(p)) {
                // 縮小堆空間
                shrink_new_heap(p, wanted, objv, nobj);
                adjust_size = p->htop - p->heap;
            }
        }
        else if (need_after > HEAP_SIZE(p)) {
            // 增加堆空間
            grow_new_heap(p, next_heap_size(p, need_after, 0), objv, nobj);
            adjust_size = p->htop - p->heap;
        }
        // 計算消耗的reduction值
        return gc_cost(size_after, adjust_size);
    }
    // 沒有足夠的空間進行小回收,則當前函數return-1, 外層呼叫會判斷並執行大回收
    return -1;
}

大回收實現核心分析

static int major_collection(Process* p, ,,,)
{
    size_before = ygen_usage; 
    size_before += p->old_htop - p->old_heap;   //年老堆中使用的記憶體
    stack_size = p->hend - p->stop;  //棧使用的記憶體
    new_sz = stack_size + size_before;  //計算總的使用空間
    new_sz = next_heap_size(p, new_sz, 0);  //從heap_sizes陣列中找到適合的記憶體空間數值
    if (MAX_HEAP_SIZE_GET(p)) {
        判斷是否設定max_heap_size,設定且超出設定,return -2;
        外部呼叫邏輯會對minor_collection的返回值進行判斷,爲-2時會幹掉當前進程
    }
    n_htop = n_heap = (Eterm *) ERTS_HEAP_ALLOC(ERTS_ALC_T_HEAP, sizeof(Eterm)*new_sz); //按new_sz申請空間
    n_htop = full_sweep_heaps(p, live_hf_end, 0, n_heap, n_htop, oh, oh_size, objv, nobj); //移動堆數據

    //移動棧數據
    stk_sz = HEAP_END(p) - p->stop;
    sys_memcpy(n_heap + new_sz - stk_sz, p->stop, stk_sz * sizeof(Eterm));
    p->stop = n_heap + new_sz - stk_sz;
    //釋放舊空間
    erts_deallocate_young_generation(p);

    HEAP_START(p) = n_heap;
    HEAP_TOP(p) = n_htop;
    HEAP_SIZE(p) = new_sz;
    HEAP_END(p) = n_heap + new_sz;
    GEN_GCS(p) = 0;  //小gc計數清0
    HIGH_WATER(p) = HEAP_TOP(p);  //設定水位線,下一次gc,水位線下仍然存活的數據將被移動到年老堆

    if (p->sig_qs.flags & FS_ON_HEAP_MSGQ)
        move_msgs_to_heap(p);  //若設定了on_heap,則將存在進程外的msg移動到進程堆中
    size_after = HEAP_TOP(p) - HEAP_START(p) + p->mbuf_sz;
    adjusted = adjust_after_fullsweep(p, need, objv, nobj); //下文分析
    //計算使用的reduction
    return gc_cost(size_after, adjusted ? size_after : 0);
}

adjust_after_fullsweep函數解析:

static int adjust_after_fullsweep(Process *p, ...)
{
    int adjusted = 0;
    Uint wanted, sz, need_after;
    Uint stack_size = STACK_SZ_ON_HEAP(p);
    
    need_after = (HEAP_TOP(p) - HEAP_START(p)) + need + stack_size;
    //need_after 可以理解爲當前需要的空間大小
    if (HEAP_SIZE(p) < need_after) {
        // 剩餘空間不滿足need_after的需求,則堆需要增長。增長的期望值爲當前的需求值
        sz = next_heap_size(p, need_after, 0);
        grow_new_heap(p, sz, objv, nobj);
        adjusted = 1;
    } else if (3 * HEAP_SIZE(p) < 4 * need_after){
        //當前使用的空間超過當前分配堆空間的百分之75,則標記下次gc時需要增長空間
        FLAGS(p) |= F_HEAP_GROW;
    } else if (4 * need_after < HEAP_SIZE(p) && HEAP_SIZE(p) > H_MIN_SIZE){
        //當前使用的空間小於當前分配的空間的百分之25,則縮小
        //縮小的期望值爲兩倍當前的需求值
        wanted = 2 * need_after;
        sz = wanted < p->min_heap_size ? p->min_heap_size : next_heap_size(p, wanted, 0);
        if (sz < HEAP_SIZE(p)) {
            shrink_new_heap(p, sz, objv, nobj);
            adjusted = 1;
        }
    }
    return adjusted;
}

最後看看grow_new_heap是如何增長堆區空間的, shrink_new_heap類似就不多做分析

static void grow_new_heap(Process *p, Uint new_sz, Eterm* objv, int nobj)
{
    Eterm* new_heap;
    Uint heap_size = HEAP_TOP(p) - HEAP_START(p);  //堆區大小
    Uint stack_size = p->hend - p->stop;  //棧區大小
    Sint offs;

    new_heap = (Eterm *) ERTS_HEAP_REALLOC(ERTS_ALC_T_HEAP,
                       (void *) HEAP_START(p),
                       sizeof(Eterm)*(HEAP_SIZE(p)),  //這個參數沒啥用
                       sizeof(Eterm)*new_sz);
    //申請從地址HEAP_START(p)開始,大小爲sizeof(Eterm)*new_sz的連續記憶體空間
    //如果HEAP_START(p)地址後的空間不夠,則會分配新的記憶體,並將當前空間的數據複製到新記憶體,並自動釋放掉舊記憶體
    if ((offs = new_heap - HEAP_START(p)) == 0) { //原地擴充空間
        HEAP_END(p) = new_heap + new_sz;
        sys_memmove(p->hend - stack_size, p->stop, stack_size * sizeof(Eterm)); //調整棧區位置
        p->stop = p->hend - stack_size;
    } else { //分配了新空間, 即HEAP_START(p)發生了變化,則需要調整相關指針的指向
        char* area = (char *) HEAP_START(p);
        Uint area_size = (char *) HEAP_TOP(p) - area;
        Eterm* prev_stop = p->stop;
        offset_heap(new_heap, heap_size, offs, area, area_size);
        HIGH_WATER(p) = new_heap + (HIGH_WATER(p) - HEAP_START(p)); //調整水位線的指向
        HEAP_END(p) = new_heap + new_sz;
        prev_stop = new_heap + (p->stop - p->heap);
        p->stop = p->hend - stack_size;
        sys_memmove(p->stop, prev_stop, stack_size * sizeof(Eterm)); //調整棧區位置
        offset_rootset(p, offs, area, area_size, objv, nobj);
        HEAP_TOP(p) = new_heap + heap_size;
        HEAP_START(p) = new_heap;
    }
    HEAP_SIZE(p) = new_sz;
}