erlang garbage_collect 源码剖析

两个词可以概括整个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的增长率增长。
此处贴出heap_sizes数组的部分数据:

12, 38, 51, 90, 142, 233, 376, 610, 987, 1598, 2586, 4185, 6772, 10958, 17731, 28690, 46422, 
75113, 121536, 196650, 318187, 514838, 833026, 999631, 1199557, 1439468, 1727361, 2072833, 
2487399, 2984878, 3581853, 4298223, 5157867, 6189440, 7427328, 8912793, 10695351, 12834421, 
15401305, 18481566, 22177879, 26613454, 31936144, 38323372, 45988046, 55185655, 66222786, 
79467343, 95360811, 114432973, 137319567, 164783480, 197740176, 237288211, 284745853, 341695023, 
410034027, 492040832, 590448998, 708538797, 850246556, 1020295867, 1224355040, 1469226048, 
1763071257, 2115685508, 2538822609, 3046587130, 3655904556, 4387085467, 5264502560, 6317403072, 
7580883686, 9097060423, 10916472507, 13099767008, 15719720409, 18863664490, 22636397388, 
27163676865, 32596412238, 39115694685, 46938833622, 56326600346,...

下面为小回收的实现核心分析

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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值