1. 程式人生 > >記一次 JVM 原始碼分析(3.記憶體管理與GC)

記一次 JVM 原始碼分析(3.記憶體管理與GC)

簡介

miniJVM 的記憶體管理的實現較為簡單

  • 記憶體分配使用了開源的 ltalloc 庫
  • GC就是經典的 Mark-Sweep GC

物件分配

物件分配要關注的就兩個過程

  • New 一個 Java 物件的過程
  • 記憶體塊在堆上分配的過程

物件在 JVM 堆中的存在形式

首先,每個物件在 JVM 的結構體的頭部都有一個叫 MemoryBlock 的頭部 這個頭部描述了物件的型別,讀寫鎖,以及 GC 標誌

//記憶體塊的頭部描述,每個可分配的記憶體塊都在頭部有此結構體,以描述記憶體塊的型別和狀態
typedef struct _MemoryBlock {
    //物件對應的類
    JClass *
clazz; struct _MemoryBlock *next; ThreadLock *volatile thread_lock; //型別,是引用物件還是 JClass 還是其他型別 u8 type;//type of array or object runtime,class //GC 標誌 u8 garbage_mark; u8 garbage_reg; u8 arr_type_index; } MemoryBlock;

現有的物件只有兩種型別

  • JClass,也就是類物件
  • 0bject,也就是普通物件
struct _ClassType {
//記憶體塊描述頭部 MemoryBlock mb; ..... } struct _InstanceType { MemoryBlock mb; //型別二選一 union { //成員變數記憶體開始的地址 c8 *obj_fields; //object fieldRef body c8 *arr_body;//array body }; //陣列長度 s32 arr_length; };

例項化物件

普通物件例項化

對於普通物件的例項化,流程非常簡單

  • 在堆上為物件分配記憶體,大小為物件內部非靜態成員變數的總大小(包括各個父類的)+ Instance 結構體本身的大小
  • 將物件引用註冊到 GC 管理中

物件記憶體結構 在這裡插入圖片描述

Instance *instance_create(Runtime *runtime, JClass *clazz) {
    //很簡單,分配目標類中的成員變數所佔記憶體大小的記憶體空間 + Instance 結構體本身所佔空間 就可以了。
    Instance *ins = jvm_calloc(sizeof(Instance) + clazz->field_instance_len);
    ins->mb.type = MEM_TYPE_INS;
    ins->mb.clazz = clazz;
    //指向成員變數記憶體開始的地址
    ins->obj_fields = (c8 *) (&ins[1]);//jvm_calloc(clazz->field_instance_len);

    gc_refer_reg(runtime, ins);
    return ins;
}

Class 物件例項化

每個java 類有一個 java.lang.Class 的例項, 用於承載對相關java類的操作 這個類相當於 JClass 結構體在 Java 層中的 mirro,一對一的關係

  • 先檢查 JClass 中有沒有已經例項化的 Class 物件,有的話直接返回
  • New Class 的 Instance
  • 加入 GC Holder 防止被 GC
  • 呼叫 Class 物件的預設無參建構函式 及 初始化成員變數值
  • JClass 結構體和 Class 物件互相儲存對方的指標
Instance *insOfJavaLangClass_create_get(Runtime *runtime, JClass *clazz) {
    JClass *java_lang_class = classes_load_get_c(STR_CLASS_JAVA_LANG_CLASS, runtime);
    if (java_lang_class) {
        if (clazz->ins_class) {
            return clazz->ins_class;
        } else {
            //New Class 物件
            Instance *ins = instance_create(runtime, java_lang_class);
            //加入 GC_Holder 防止被 GC
            gc_refer_hold(ins);
            //呼叫預設無參建構函式 及 初始化成員變數值
            instance_init(ins, runtime);
            //在 JClass 結構體中儲存 Class 物件的指標
            clazz->ins_class = ins;
            //在 Class 物件中儲存 JClass 結構體的指標
            insOfJavaLangClass_set_classHandle(ins, clazz);
            return ins;
        }
    }
    return NULL;
}

void insOfJavaLangClass_set_classHandle(Instance *insOfJavaLangClass, JClass *handle) {
    setFieldLong(getInstanceFieldPtr(insOfJavaLangClass, jvm_runtime_cache.class_classHandle), (s64) (intptr_t) handle);
}

String 物件初始化

String 物件除了普通物件的內容以外,還有兩個其他的操作

  • .class 檔案中的字串是以 UTF-8 編碼,而 Java 在執行時字串應該是 Unicode 形式,所以這裡需要將 UTF-8 轉換成 Unicode
  • String 物件中有一個 char[] 陣列用來儲存字串,這裡需要初始化 char[] 物件並且將其設定到 String 中的 value 成員變數中去
//===============================    例項化字串  ==================================
Instance *jstring_create(Utf8String *src, Runtime *runtime) {
    if (!src)return NULL;
    Utf8String *clsName = utf8_create_c(STR_CLASS_JAVA_LANG_STRING);
    JClass *jstr_clazz = classes_load_get(clsName, runtime);
    Instance *jstring = instance_create(runtime, jstr_clazz);
    gc_refer_hold(jstring);//hold for no gc

    jstring->mb.clazz = jstr_clazz;
    instance_init(jstring, runtime);

    c8 *ptr = jstring_get_value_ptr(jstring);
    u16 *buf = jvm_calloc(src->length * data_type_bytes[DATATYPE_JCHAR]);
    //UTF-8 -> Unicode
    s32 len = utf8_2_unicode(src, buf);
    if (len >= 0) {//可能解析出錯
        //填充 String 中的 char 陣列
        Instance *arr = jarray_create_by_type_index(runtime, len, DATATYPE_JCHAR);//u16 type is 5
        setFieldRefer(ptr, (__refer) arr);//設定陣列
        memcpy(arr->arr_body, buf, len * data_type_bytes[DATATYPE_JCHAR]);
    }
    jvm_free(buf);
    jstring_set_count(jstring, len);//設定長度
    utf8_destory(clsName);
    gc_refer_release(jstring);
    return jstring;
}

例項化陣列

Java 中每種陣列也是一個單獨的類

  • 陣列物件例項化與普通物件的區別在於多了一個計算記憶體大小的步驟
  • 陣列物件在記憶體中以 Instance 結構體 + 陣列體存在 在這裡插入圖片描述 Code
//===============================    例項化陣列  ==================================
Instance *jarray_create_by_class(Runtime *runtime, s32 count, JClass *clazz) {
    //型別的 index
    s32 typeIdx = clazz->mb.arr_type_index;
    //取得對應型別所佔記憶體大小
    s32 width = data_type_bytes[typeIdx];
    //為陣列物件分配記憶體,大小為 元素大小 * 長度
    Instance *arr = jvm_calloc(sizeof(Instance) + (width * count));
    //配置 memblock 屬性
    arr->mb.type = MEM_TYPE_ARR;
    arr->mb.clazz = clazz;
    arr->mb.arr_type_index = typeIdx;
    arr->arr_length = count;
    //指向陣列體
    if (arr->arr_length)arr->arr_body = (c8 *) (&arr[1]);
    gc_refer_reg(runtime, arr);
    return arr;
}

對陣列體大小的計算,每種陣列元素的大小對應其物件型別的大小,也就是基本資料型別或者引用型別:

s32 data_type_bytes[DATATYPE_COUNT] = {0, 0, 0, 0,
                                       sizeof(c8),
                                       sizeof(u16),
                                       sizeof(f32),
                                       sizeof(f64),
                                       sizeof(c8),
                                       sizeof(s16),
                                       sizeof(s32),
                                       sizeof(s64),
                                       sizeof(__refer),
                                       sizeof(__refer),
};

多維陣列

對於多維陣列,需要遞迴遍歷的去走建立緯度 - 1 的陣列:

Instance *jarray_multi_create(Runtime *runtime, s32 *dim, s32 dim_size, Utf8String *pdesc, s32 deep) {
    s32 len = dim[dim_size - 1 - deep];
    if (len == -1) {
        return NULL;
    }
    JClass *cl = array_class_create_get(runtime, pdesc);

    //獲取或者建立普通陣列
    Instance *arr = jarray_create_by_class(runtime, len, cl);
    //維度 - 1
    Utf8String *desc = utf8_create_part(pdesc, 1, pdesc->length - 1);
    
    c8 ch = utf8_char_at(desc, 0);
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
    jvm_printf("multi arr deep :%d  type(%c) arr[%x] size:%d\n", deep, ch, arr, len);
#endif
    //如果還有維度,則繼續遞迴
    if (ch == '[') {
        int i;
        s64 val;
        for (i = 0; i < len; i++) {
            Instance *elem = jarray_multi_create(runtime, dim, dim_size, desc, deep + 1);
            val = (intptr_t) elem;
            jarray_set_field(arr, i, val);
        }
    }
    utf8_destory(desc);
    return arr;
}

內部異常例項化

JVM 在執行時會丟擲一些內部異常,例如著名的 StackOverflowException,這些異常都不是在 Java 程式碼中 New 出來的,那麼就需要在 JVM 執行中去直接 New 出來對應內部異常物件拋給 Java 層。 分為兩種:

  • StackOverflowException 這種是不會帶 String 型別的提示資訊的
  • 還有一些是有提示資訊 Msg 的 這裡直接看帶提示資訊的實現:
Instance *exception_create_str(s32 exception_type, Runtime *runtime, c8 *errmsg) {
#if _JVM_DEBUG_BYTECODE_DETAIL > 5
    jvm_printf("create exception : %s\n", STRS_CLASS_EXCEPTION[exception_type]);
#endif
    //New String 物件用作異常提示
    Utf8String *uerrmsg = utf8_create_c(errmsg);
    Instance *jstr = jstring_create(uerrmsg, runtime);
    gc_refer_hold(jstr);
    utf8_destory(uerrmsg);
    //String 入引數棧
    RuntimeStack *para = stack_create(1);
    push_ref(para, jstr);
    gc_refer_release(jstr);
    //New 異常物件
    Utf8String *clsName = utf8_create_c(STRS_CLASS_EXCEPTION[exception_type]);
    JClass *clazz = classes_load_get(clsName, runtime);
    utf8_destory(clsName);
    Instance *ins = instance_create(runtime, clazz);
    gc_refer_hold(ins);
    //呼叫異常物件的帶 String 引數的構造方法,傳入 Msg
    instance_init_methodtype(ins, runtime, "(Ljava/lang/String;)V", para);
    gc_refer_release(ins);
    stack_destory(para);
    return ins;
}

lambda 表示式例項化

lambda 表示式相當於一個動態生成的方法物件,那麼自然也是需要例項化的 lambda 表示式型別由三個JVM內部型別組成:

  • MethodHandle,它是可對直接執行的方法(或域、構造方法等)的型別的引用
  • MethodType, 它是表示方法簽名型別的不可變物件。每個方法控制代碼都有一個MethodType例項,用來指明方法的返回型別和引數型別。
  • MethodHandles$Lookup,相當於MethodHandle工廠類,通過findxxx方法可以得到相應的MethodHandle,還可以配合反射API建立MethodHandle,對應的方法有unreflect、unreflectSpecial等
//MethodType 包含一個 String 型別的方法描述
Instance *method_type_create(Runtime *runtime, Utf8String *desc) {
    JClass *cl = classes_load_get_c(STR_CLASS_JAVA_LANG_INVOKE_METHODTYPE, runtime);
    if (cl) {
        Instance *mt = instance_create(runtime, cl);
        gc_refer_hold(mt);
        Instance *jstr_desc = jstring_create(desc, runtime);
        RuntimeStack *para = stack_create(1);
        //String 引數入棧
        push_ref(para, jstr_desc);
        //呼叫構造方法
        instance_init_methodtype(mt, runtime, "(Ljava/lang/String;)V", para);
        stack_destory(para);
        gc_refer_release(mt);
        return mt;
    }
    return NULL;
}
//Lookup 包含 lambda 表示式方法所在的類
Instance *method_handles_lookup_create(Runtime *runtime, JClass *caller) {
    JClass *cl = classes_load_get_c(STR_CLASS_JAVA_LANG_INVOKE_METHODHANDLES_LOOKUP, runtime);
    if (cl) {
        Instance *lookup = instance_create(runtime, cl);
        gc_refer_hold(lookup);
        RuntimeStack *para = stack_create(1);

        push_ref(para, insOfJavaLangClass_create_get(runtime, caller));
        instance_init_methodtype(lookup, runtime, "(Ljava/lang/Class;)V", para);
        stack_destory(para);
        gc_refer_release(lookup);
        return lookup;
    }
    return NULL;
}
//handler 較為複雜,包含了具體的方法資訊,有4個引數
Instance *method_handle_create(Runtime *runtime, MethodInfo *mi, s32 kind) {
    JClass *cl = classes_load_get_c(STR_CLASS_JAVA_LANG_INVOKE_METHODHANDLE, runtime);
    if (cl) {
        Instance *mh = instance_create(runtime, cl);
        gc_refer_hold(mh);
        RuntimeStack *para = stack_create(4);
        //型別
        push_int(para, kind);
        //當前類名
        Instance *jstr_clsName = jstring_create(mi->_this_class->name, runtime);
        gc_refer_hold(jstr_clsName);
        push_ref(para, jstr_clsName);
        //lambda 方法名
        Instance *jstr_methodName = jstring_create(mi->name, runtime);
        push_ref(para, jstr_methodName);
        gc_refer_hold(jstr_methodName);
        //方法的描述
        Instance *jstr_methodDesc = jstring_create(mi->descriptor, runtime);
        push_ref(para, jstr_methodDesc);
        gc_refer_hold(jstr_methodDesc);
        instance_init_methodtype(mh, runtime, "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", para);
        stack_destory(para);
        gc_refer_release(mh);
        gc_refer_release(jstr_clsName);
        gc_refer_release(jstr_methodName);
        gc_refer_release(jstr_methodDesc);
        return mh;
    }
    return NULL;
}

記憶體分配

記憶體管理使用的是 ltalloc 的開源庫 地址在 http://code.google.com/p/ltalloc 這裡不做深入剖析 這裡就簡單看一下記憶體分配的外層方法 d_type.jvm_calloc -> ltalloc.ltcalloc -> ltmalloc

CPPCODE(template <bool throw_> static) void *ltmalloc(size_t size)
{
	unsigned int sizeClass = get_size_class(size);
	ThreadCache *tc = &threadCache[sizeClass];
	FreeBlock *fb = tc->freeList;
	if (likely(fb))
	{
		tc->freeList = fb->next;
		tc->counter++;
		return fb;
	}
	else
		return fetch_from_central_cache CPPCODE(<throw_>)(size, tc, sizeClass);
}

這裡大致可以查看出來 ltalloc 堆空間可以分為兩個部分

  • 每個執行緒都有一個私有的小堆,這樣是避免多執行緒分派物件的時候,由鎖帶來的效能問題,Android ART 中也有類似的設計
  • 一個所有執行緒公有的大堆,當然面對不同執行緒來的請求,一定是會加鎖的

GC

miniJVM 的 GC 使用的是經典的 Mark-Sweep 標記清除演算法,僅支援單執行緒收集,並不支援併發。

GC 流程

概括:

  • 當物件被建立時,註冊進垃圾收集器,納入監管體系(註冊的物件包括 Class 類, Instance 物件例項(包括陣列物件))
  • 在垃圾回收時,由垃圾收集執行緒來收集,收集 collector->header 連結串列中的物件,收集方法為: 當物件未被任一執行緒引用時,進行標記,直接銷燬,釋放記憶體
  • 收集執行緒會暫停所有正在執行中的java執行緒 ,回收完之後,恢復執行緒執行
  • jdwp除錯執行緒中的執行時物件不可回收

流程

  • 暫停所有執行緒,stop the world
  • 將所有的註冊 reg/hold/release 的物件新增到一個臨時列表中
  • 拷貝所有執行時的引用
  • 標記所有被執行緒引用的物件
  • 恢復所有執行緒執行,resume the world
  • 處理,呼叫 finalize 方法
  • 釋放沒有被標記的物件(即不可達物件)
  • 將臨時連結串列恢復為主連結串列
/**
 * 查詢所有例項,如果發現沒有被引用時 mb->garbage_mark ,
 * 去除掉此物件對其他物件的引用,並銷燬物件
 *
 * @return ret
 */
s64 garbage_collect() {
    collector->isgc = 1;
    s64 mem_total = 0, mem_free = 0;
    s64 del = 0;
    s64 time, start;

    start = time = currentTimeMillis();
    //prepar gc resource ,
    //GC 同步鎖
    garbage_thread_lock();

    //Stop the world 暫停所有執行緒
    if (_garbage_pause_the_world() != 0) {
        _garbage_resume_the_world();
        return -1;
    }
//    jvm_printf("garbage_pause_the_world %lld\n", (currentTimeMillis() - time));
//    time = currentTimeMillis();
    if (collector->tmp_header) {
        collector->tmp_tailer->next = collector->header;//接起來
        collector->header = collector->tmp_header;
        collector->tmp_header = NULL;
        collector->tmp_tailer = NULL;
    }
//    jvm_printf("garbage_move_cache %lld\n", (currentTimeMillis() - time));
//    time = currentTimeMillis();
    //拷貝所有引用到 GC 連結串列中
    _garbage_copy_refer();
    //
//    jvm_printf("garbage_copy_refer %lld\n", (currentTimeMillis() - time));
//    time = currentTimeMillis();
    //real GC start
    //開始 GC,設定 FLAG
    _garbage_change_flag();
    //開始遞迴搜尋已經拷貝的根引用
    _garbage_big_search();
    //
//    jvm_printf("garbage_big_search %lld\n", (currentTimeMillis() - time));
//    time = currentTimeMillis();

    //java 執行緒恢復執行,這時候已經標記了那些物件需要回收了,所以不再需要暫停執行緒
    _garbage_resume_the_world();
    garbage_thread_unlock();

//    jvm_printf("garbage_resume_the_world %lld\n", (currentTimeMillis() - time));

    s64 time_stopWorld = currentTimeMillis() - start;
    time = currentTimeMillis();
    //

    //處理 finalize 方法
    MemoryBlock *nextmb = collector->header;
    MemoryBlock *curmb, *prevmb = NULL;
    //finalize
    if (collector->_garbage_thread_status == GARBAGE_THREAD_NORMAL) {
        while (nextmb) {
            curmb = nextmb;
            nextmb = curmb->next;
            if (curmb->clazz->finalizeMethod) {// there is a method called finalize
                if (curmb->type == MEM_TYPE_INS && curmb->garbage_mark != collector->flag_refer) {
                    //這裡回去呼叫每個物件的 finalize 方法
                    instance_finalize((Instance *) curmb, collector->runtime);
                }
            }
        }
    }

    //在 finalize 建立的物件,是在 gc 執行緒建立的物件。。。也需要被註冊
    gc_move_refer_thread_2_gc(collector->runtime);// maybe someone new object in finalize...

//    jvm_printf("garbage_finalize %lld\n", (currentTimeMillis() - time));
//    time = currentTimeMillis();
    //clear