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: 
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容