QuickJS 是一個輕量級的 JavaScript 引擎矾兜,可以代替 V8 實現(xiàn) JS 腳本的執(zhí)行寸宵,如果要使用 QuickJS檩帐,必須要弄懂其垃圾回收原理术幔,否則容易出現(xiàn)野指針或內(nèi)存泄漏,從而導(dǎo)致程序崩潰湃密,本文通過源碼剖析 QuickJS 的垃圾回收原理诅挑。
引用計數(shù)法
QuickJS 是使用引用計數(shù)法來判斷對象是否可以被釋放,引用計數(shù)法非常簡單泛源,通過給對象分配一個計時器來保存該對象被引用的次數(shù)拔妥,如果該對象被其它對象引用就會加1,如果刪除引用就會減1达箍,當(dāng)引用的計數(shù)器為0時没龙,那么就會被回收。
QuickJS基礎(chǔ)用法
JSRuntime
JSRuntime 是 QuickJS 最底層的執(zhí)行環(huán)境缎玫,不使用的時需要及時釋放硬纤。
// 創(chuàng)建 JSRuntime
JSRuntime *runtime = JS_NewRuntime();
// 釋放 JSRuntime
JS_FreeRuntime(runtime);
JSContext
一個 JSRuntime 可以創(chuàng)建多個 Context,每個 Context 之間是相互隔離的赃磨,不使用的時需要及時釋放筝家。
// 創(chuàng)建 JSContext
JSContext *ctx = JS_NewContext(runtime);
// 釋放 JSContext
JS_FreeContext(ctx);
JSValue
如果我們需要自己創(chuàng)建和關(guān)聯(lián)JS對象時,我們需要處理好引用問題邻辉,必須通過 c 創(chuàng)建一個JSValue對象溪王,那么我們就需要手動釋放它,否則就會導(dǎo)致內(nèi)存泄漏值骇,同時我們也不能多次釋放莹菱,這也會導(dǎo)致野指針,從而導(dǎo)致程序崩潰雷客,如果我們只是純粹運行js腳本就無需我們關(guān)心這個問題芒珠,引擎已經(jīng)處理好了。
// 創(chuàng)建對象搅裙,引用+1
JSValue jsValue = JS_NewObject(ctx);
// 引用+1
JS_DupValue(ctx, value);
// 引用-1
JS_FreeValue(ctx, value);
剖析引擎垃圾回收
通過上面示例皱卓,我們得知引用計數(shù)法是通過JS_DupValue記錄引用+1,JS_FreeValue引用減1實現(xiàn)計數(shù)部逮,接下來就通過源碼分析如何實現(xiàn)娜汁。
JSRefCountHeader
引用計數(shù)器頭是一個結(jié)構(gòu)體,目前只有一個int值兄朋,用于記錄對象的引用次數(shù)掐禁。
typedef struct JSRefCountHeader {
int ref_count;
} JSRefCountHeader;
JS_DupValue
引用計數(shù)器+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 處理引用計數(shù)器-1,如果引用屬于小于0時候就會執(zhí)行垃圾回收,這里引入引用計數(shù)器最大的問題傅事,如果a引用b缕允,b也引用了a,這樣的相互應(yīng)用是不是就會導(dǎo)致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 函數(shù)就是用來解決相互引用問題障本,會在特定的時機觸發(fā)。
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 函數(shù)會把子屬性引用等于 0 的對象從gc_obj_list 移動到 tmp_obj_list;
- 如果發(fā)現(xiàn)元素的引用等于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忿项,就說明當(dāng)前是在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
經(jīng)過上面兩個函數(shù)洒擦,tmp_obj_list 就只會剩下環(huán)形引用的對象,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 觸發(fā)時機
- JS_FreeRuntime() 就是引擎被釋放的時候會觸發(fā)秦踪;
- 創(chuàng)建新的對象時會調(diào)用 js_trigger_gc 函數(shù)褐捻,當(dāng)引擎占用的內(nèi)存malloc_size大于閥值malloc_gc_threshold時候就會觸發(fā);
malloc_gc_threshold 的起始大小為 256 * 1024椅邓,也可以通過 JS_SetGCThreshold() 設(shè)置自動GC的觸發(fā)大小柠逞,如果傳入-1就不會自動執(zhí)行JS_RunGC;閥值也會隨著JS_RunGC之后就發(fā)生變化景馁,malloc_size + malloc_size >> 1板壮,就是當(dāng)前占用內(nèi)存的 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屬性應(yīng)用+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: