QuickJS 源码剖析:垃圾回收原理

QuickJS 是一个轻量级的 JavaScript 引擎,可以代替 V8 实现 JS 脚本的执行,如果要使用 QuickJS,必须要弄懂其垃圾回收原理,否则容易出现野指针或内存泄漏,从而导致程序崩溃,本文通过源码剖析 QuickJS 的垃圾回收原理。

引用计数法

QuickJS 是使用引用计数法来判断对象是否可以被释放,引用计数法非常简单,通过给对象分配一个计时器来保存该对象被引用的次数,如果该对象被其它对象引用就会加1,如果删除引用就会减1,当引用的计数器为0时,那么就会被回收。

QuickJS基础用法

JSRuntime

JSRuntime 是 QuickJS 最底层的执行环境,不使用的时需要及时释放。

// 创建 JSRuntime
JSRuntime *runtime = JS_NewRuntime();
// 释放 JSRuntime
JS_FreeRuntime(runtime);

JSContext

一个 JSRuntime 可以创建多个 Context,每个 Context 之间是相互隔离的,不使用的时需要及时释放。

// 创建 JSContext
JSContext *ctx = JS_NewContext(runtime);
// 释放 JSContext
JS_FreeContext(ctx);

JSValue

如果我们需要自己创建和关联JS对象时,我们需要处理好引用问题,必须通过 c 创建一个JSValue对象,那么我们就需要手动释放它,否则就会导致内存泄漏,同时我们也不能多次释放,这也会导致野指针,从而导致程序崩溃,如果我们只是纯粹运行js脚本就无需我们关心这个问题,引擎已经处理好了。

// 创建对象,引用+1
JSValue jsValue = JS_NewObject(ctx);
// 引用+1
JS_DupValue(ctx, value);
// 引用-1
JS_FreeValue(ctx, value);

剖析引擎垃圾回收

通过上面示例,我们得知引用计数法是通过JS_DupValue记录引用+1,JS_FreeValue引用减1实现计数,接下来就通过源码分析如何实现。

JSRefCountHeader

引用计数器头是一个结构体,目前只有一个int值,用于记录对象的引用次数。

typedef struct JSRefCountHeader {
    int ref_count;
} JSRefCountHeader;

JS_DupValue

引用计数器+1

static inline JSValue JS_DupValue(JSContext *ctx, JSValueConst v){
    if (JS_VALUE_HAS_REF_COUNT(v)) {
        JSRefCountHeader *p = (JSRefCountHeader *)JS_VALUE_GET_PTR(v);
        p->ref_count++;
    }
    return (JSValue)v;
}

JS_FreeValue

JS_FreeValue 处理引用计数器-1,如果引用属于小于0时候就会执行垃圾回收,这里引入引用计数器最大的问题,如果a引用b,b也引用了a,这样的相互应用是不是就会导致a和b都无法回收?

static inline void JS_FreeValue(JSContext *ctx, JSValue v){
    if (JS_VALUE_HAS_REF_COUNT(v)) {
        JSRefCountHeader *p = (JSRefCountHeader *)JS_VALUE_GET_PTR(v);
        if (--p->ref_count <= 0) {
            __JS_FreeValue(ctx, v);
        }
    }
}

JS_RunGC

JS_RunGC 函数就是用来解决相互引用问题,会在特定的时机触发。

void JS_RunGC(JSRuntime *rt){
    /* decrement the reference of the children of each object. mark = 1 after this pass. */
    gc_decref(rt);
    /* keep the GC objects with a non zero refcount and their childs */
    gc_scan(rt);
    /* free the GC objects in a cycle */
    gc_free_cycles(rt);
}
gc_decref
  • 遍历gc_obj_list,通过 mark_children() 对元素的子属性引用-1;
  • gc_decref_child 函数会把子属性引用等于 0 的对象从gc_obj_list 移动到 tmp_obj_list;
  • 如果发现元素的引用等于0,也把元素从 gc_obj_list 移动到tmp_obj_list;
static void gc_decref(JSRuntime *rt)
{
    struct list_head *el, *el1;
    JSGCObjectHeader *p;
    
    init_list_head(&rt->tmp_obj_list);

    /* decrement the refcount of all the children of all the GC
       objects and move the GC objects with zero refcount to
       tmp_obj_list */
    list_for_each_safe(el, el1, &rt->gc_obj_list) {
        p = list_entry(el, JSGCObjectHeader, link);
        assert(p->mark == 0);
        mark_children(rt, p, gc_decref_child);
        p->mark = 1;
        if (p->ref_count == 0) {
            list_del(&p->link);
            list_add_tail(&p->link, &rt->tmp_obj_list);
        }
    }
}
gc_scan
  • gc_scan_incref_child 对 gc_obj_list 的元素的每一个子属性的引用+1;
  • 如果子属性的引用等于1,就说明当前是在tmp_obj_list,需要把子属性从tmp_obj_list 移动回 gc_obj_list;
  • gc_scan_incref_child 对 tmp_obj_list的对象的属性的引用+1;
static void gc_scan(JSRuntime *rt)
{
    struct list_head *el;
    JSGCObjectHeader *p;

    /* keep the objects with a refcount > 0 and their children. */
    list_for_each(el, &rt->gc_obj_list) {
        p = list_entry(el, JSGCObjectHeader, link);
        assert(p->ref_count > 0);
        p->mark = 0; /* reset the mark for the next GC call */
        mark_children(rt, p, gc_scan_incref_child);
    }
    
    /* restore the refcount of the objects to be deleted. */
    list_for_each(el, &rt->tmp_obj_list) {
        p = list_entry(el, JSGCObjectHeader, link);
        mark_children(rt, p, gc_scan_incref_child2);
    }
}
gc_free_cycles

经过上面两个函数,tmp_obj_list 就只会剩下环形引用的对象,gc_free_cycles() 回收 tmp_obj_list 列表的对象,并且对属性的引用-1。

static void gc_free_cycles(JSRuntime *rt)
{
    struct list_head *el, *el1;
    JSGCObjectHeader *p;
#ifdef DUMP_GC_FREE
    BOOL header_done = FALSE;
#endif

    rt->gc_phase = JS_GC_PHASE_REMOVE_CYCLES;

    for(;;) {
        el = rt->tmp_obj_list.next;
        if (el == &rt->tmp_obj_list)
            break;
        p = list_entry(el, JSGCObjectHeader, link);
        /* Only need to free the GC object associated with JS
           values. The rest will be automatically removed because they
           must be referenced by them. */
        switch(p->gc_obj_type) {
        case JS_GC_OBJ_TYPE_JS_OBJECT:
        case JS_GC_OBJ_TYPE_FUNCTION_BYTECODE:
#ifdef DUMP_GC_FREE
            if (!header_done) {
                printf("Freeing cycles:\n");
                JS_DumpObjectHeader(rt);
                header_done = TRUE;
            }
            JS_DumpGCObject(rt, p);
#endif
            free_gc_object(rt, p);
            break;
        default:
            list_del(&p->link);
            list_add_tail(&p->link, &rt->gc_zero_ref_count_list);
            break;
        }
    }
    rt->gc_phase = JS_GC_PHASE_NONE;
           
    list_for_each_safe(el, el1, &rt->gc_zero_ref_count_list) {
        p = list_entry(el, JSGCObjectHeader, link);
        assert(p->gc_obj_type == JS_GC_OBJ_TYPE_JS_OBJECT ||
               p->gc_obj_type == JS_GC_OBJ_TYPE_FUNCTION_BYTECODE);
        js_free_rt(rt, p);
    }

    init_list_head(&rt->gc_zero_ref_count_list);
}

JS_RunGC 触发时机

  1. JS_FreeRuntime() 就是引擎被释放的时候会触发;
  2. 创建新的对象时会调用 js_trigger_gc 函数,当引擎占用的内存malloc_size大于阀值malloc_gc_threshold时候就会触发;

malloc_gc_threshold 的起始大小为 256 * 1024,也可以通过 JS_SetGCThreshold() 设置自动GC的触发大小,如果传入-1就不会自动执行JS_RunGC;阀值也会随着JS_RunGC之后就发生变化,malloc_size + malloc_size >> 1,就是当前占用内存的 1.5 倍。

流程

global.c = c
a.b = b
b.a = a
a.c = c
a.d = d

原始情况
gc_obj_list: global = ∞, a = 1, b = 1, c = 2, d = 1
tmp_obj_list: 

gc_decref(rt): 对gc_obj_list元素的属性引用-1,等于0的移动到tmp_obj_list
gc_obj_list: global = ∞
tmp_obj_list: a = 0, b = 0, c = 0, d = 0

gc_scan(rt): 对gc_obj_list元素的属性引用+1,等于1的移动回gc_obj_list,并且对tmp_obj_list属性应用+1
gc_obj_list: global = ∞, c = 2, d = 1
tmp_obj_list: a = 1, b = 1

gc_free_cycles(rt): 回收 tmp_obj_list 列表的对象,并且对属性的引用-1
gc_obj_list: global = ∞, c = 1
tmp_obj_list: 

你可能感兴趣的:(QuickJS 源码剖析:垃圾回收原理)