1. 程式人生 > >Java Dalvik簡要學習總結

Java Dalvik簡要學習總結

1概要

在這裡插入圖片描述
Dalivik虛擬機器有兩個堆,包括zygote堆和Active 堆,前者是不太變化的,是從父程序公用的相同實體記憶體的堆,因為很少發生寫操作在這個堆上,所以寫時複製機制不會發生在這個堆上,這也是android執行時比較巧妙的地方。

Live Heap Bitmap 和 Mark Heap Bitmap 這兩個點陣圖是是java垃圾回收機制的關鍵,用來標記java物件是否被引用的, 可以對映最大堆大小的所有物件。 live heap bitmap標記表示上次垃圾回收後存活的物件,Mark heap bitmap 標記本次垃圾回被引用的物件。有了這兩個資訊我們就知道哪些物件是需要回收的,即在live bitmap標記為1,但在mark bitmap標記為0的物件。 (為什麼需要兩個bitmap呢,因為mark bitmap被標記為0的物件有可能是不存在的物件,也有可能是要回收的物件,有了live bitmap就可以進行區分)

Mark Stack就是標記過程中物件的引用關係是錯綜複雜的,也會有迴圈引用,Mark stack通過棧的方式解決這個問題。(思路就是保證優先檢查地址小的物件)

如何標記物件: 首先java虛擬機器使用標記清除演算法,而不是引用計數方法。 這裡引入根級物件概念, 根級物件是指在gc的瞬間,被全域性變數,棧變數和暫存器引用的物件,試想 順著這些物件標記下去的物件就是全部被使用的物件,不被根級物件引用的物件其實就是沒有的。 這裡補充下引用計數存在的問題。 比如A,B兩個物件互相引用,他們的引用計數都是1,但是不被根級物件引用,這樣他們倆也是無用的物件,如存在引用計數。

Card Table : 在並行gc的過程中使用, 並行gc的過程分為3步
1 掛起所有java執行緒,gc執行緒確定根級別物件。
2 喚醒java執行緒,同時間gc執行緒標記被根級物件引用的物件,但是在這個過程中java執行緒有可能建立或者修改物件關係,這個變化的物件地址要記錄在card table中
3 gc執行緒標記完所有物件的時候,掛起所有java程序,再從card table中找到在過程2中發生變化的物件(少數物件)進行重新標記
所以 Card Tab物件的作用就是用於記錄並行gc中發生變化的物件地址

2 Dalvik 虛擬機器相關啟動引數和含義

-xms: 虛擬機器堆的起始大小,啟動時申請的實體記憶體
-xms: 虛擬機器堆使用的虛擬記憶體最大大小
-XX:HeapGrowthLimit: 減少記憶體碎片的方式
-XX:HeapMinFree 堆最小空閒值,空閒值小於該值時應該擴容堆(調整軟限制)
-XX:HeapMaxFree 堆最大空閒值,大於該值時應該堆縮容(調整軟限制)
-XX:HeapTargetUtilization 目標利用率,擴容和縮容的根據目標利用率找到合適的目標堆大小(調整軟限制)
-XX:+DisableExplicitGC 禁止顯式gc

堆最小空閒值(Min Free)、堆最大空閒值(Max Free)和堆目標利用率(Target Utilization)。這三個值可以分別通過Dalvik虛擬機器的啟動選項-XX:HeapMinFree、-XX:HeapMaxFree和-XX:HeapTargetUtilization來指定。它們用來確保每次GC之後,Java堆已經使用和空閒的記憶體有一個合適的比例,這樣可以儘量地減少GC的次數。舉個例子說,堆的利用率為U,最小空閒值為MinFree位元組,最大空閒值為MaxFree位元組。假設在某一次GC之後,存活物件佔用記憶體的大小為LiveSize。那麼這時候堆的理想大小應該為(LiveSize / U)。但是(LiveSize / U)必須大於等於(LiveSize + MinFree)並且小於等於(LiveSize + MaxFree)。

3 建立物件的記憶體申請

在這裡插入圖片描述

static void *tryMalloc(size_t size)
{
    void *ptr;
    ......

    ptr = dvmHeapSourceAlloc(size);
    if (ptr != NULL) {
        return ptr;
    }

    if (gDvm.gcHeap->gcRunning) {
        ......
        dvmWaitForConcurrentGcToComplete();
    } else {
        ......
        gcForMalloc(false);
    }

    ptr = dvmHeapSourceAlloc(size);
    if (ptr != NULL) {
        return ptr;
    }

    ptr = dvmHeapSourceAllocAndGrow(size);
    if (ptr != NULL) {
        ......
        return ptr;
    }

    gcForMalloc(true);
    ptr = dvmHeapSourceAllocAndGrow(size);
    if (ptr != NULL) {
        return ptr;
    }
   
    ......

    return NULL;
}
    1. 呼叫函式dvmHeapSourceAlloc在Java堆上分配指定大小的記憶體。如果分配成功,那麼就將分配得到的地址直接返回給呼叫者了。函式dvmHeapSourceAlloc在不改變Java堆當前大小的前提下進行記憶體分配,這是屬於輕量級的記憶體分配動作。

    2. 如果上一步記憶體分配失敗,這時候就需要執行一次GC了。不過如果GC執行緒已經在執行中,即gDvm.gcHeap->gcRunning的值等於true,那麼就直接呼叫函式dvmWaitForConcurrentGcToComplete等到GC執行完成就是了。否則的話,就需要呼叫函式gcForMalloc來執行一次GC了,引數false表示不要回收軟引用物件引用的物件。

    3. GC執行完畢後,再次呼叫函式dvmHeapSourceAlloc嘗試輕量級的記憶體分配操作。如果分配成功,那麼就將分配得到的地址直接返回給呼叫者了。

    4. 如果上一步記憶體分配失敗,這時候就得考慮先將Java堆的當前大小設定為Dalvik虛擬機器啟動時指定的Java堆最大值,再進行記憶體分配了。這是通過呼叫函式dvmHeapSourceAllocAndGrow來實現的。 

    5. 如果呼叫函式dvmHeapSourceAllocAndGrow分配記憶體成功,則直接將分配得到的地址直接返回給呼叫者了。

    6. 如果上一步記憶體分配還是失敗,這時候就得出狠招了。再次呼叫函式gcForMalloc來執行GC。引數true表示要回收軟引用物件引用的物件。

    7. GC執行完畢,再次呼叫函式dvmHeapSourceAllocAndGrow進行記憶體分配。這是最後一次努力了,成功與事都到此為止。

4 GC的型別

GC_FOR_MALLOC: 表示是在堆上分配物件時記憶體不足觸發的GC。
GC_CONCURRENT: 表示是在已分配記憶體達到一定量之後觸發的GC。
GC_EXPLICIT: 表示是應用程式呼叫System.gc、VMRuntime.gc介面或者收到SIGUSR1訊號時觸發的GC。
GC_BEFORE_OOM: 表示是在準備拋OOM異常之前進行的最後努力而觸發的GC。

實際上,GC_FOR_MALLOC、GC_CONCURRENT和GC_BEFORE_OOM三種類型的GC都是在分配物件的過程觸發的。

GC_FOR_MALLOC | GC_BEFORE_OOM 就是在我們前面看到的申請物件過程中gcForMalloc(bool soft) 函式觸發的gc

static void gcForMalloc(bool clearSoftReferences)
{
    const GcSpec *spec = clearSoftReferences ? GC_BEFORE_OOM : GC_FOR_MALLOC;
    dvmCollectGarbageInternal(spec);
}

GC_CONCURRENT 在每次物件申請成功後都會做檢查,程式碼如下

if (heap->bytesAllocated > heap->concurrentStartBytes) {
        dvmSignalCond(&gHs->gcThreadCond);
    }

GC_EXPLICIT 是明確的要進行gc

void dvmCollectGarbage()
{
    if (gDvm.disableExplicitGc) {
        return;
    }
    dvmLockHeap();
    dvmWaitForConcurrentGcToComplete();
    dvmCollectGarbageInternal(GC_EXPLICIT);
    dvmUnlockHeap();
}

從函式可以看到,GC_EXPLICIT並不一定會被執行,可以通過-XX:+DisableExplicitGC引數設定disableExplicitGc變數禁止顯方gc
另外還要等待並行gc完成,所以呼叫System.gc 執行gc的時機是不確定的

5 GC的主體流程

static void *gcDaemonThread(void* arg)
{
    dvmChangeStatus(NULL, THREAD_VMWAIT);
    dvmLockMutex(&gHs->gcThreadMutex);
    while (gHs->gcThreadShutdown != true) {
        bool trim = false;
        if (gHs->gcThreadTrimNeeded) {
            int result = dvmRelativeCondWait(&gHs->gcThreadCond, &gHs->gcThreadMutex,
                    HEAP_TRIM_IDLE_TIME_MS, 0);
            if (result == ETIMEDOUT) {
                /* Timed out waiting for a GC request, schedule a heap trim. */
                trim = true;
            }
        } else {
            dvmWaitCond(&gHs->gcThreadCond, &gHs->gcThreadMutex);
        }

        ......

        dvmLockHeap();

        if (!gDvm.gcHeap->gcRunning) {
            dvmChangeStatus(NULL, THREAD_RUNNING);
            if (trim) {
                trimHeaps();
                gHs->gcThreadTrimNeeded = false;
            } else {
                dvmCollectGarbageInternal(GC_CONCURRENT);
                gHs->gcThreadTrimNeeded = true;
            }
            dvmChangeStatus(NULL, THREAD_VMWAIT);
        }
        dvmUnlockHeap();
    }
    dvmChangeStatus(NULL, THREAD_RUNNING);
    return NULL;
}

該函式執行在gc程序中
執行完並行gc後會設定gcThreadTrimNeeded 為真,那麼過HEAP_TRIM_IDLE_TIME_MS 後會執行trimHeaps 然後再設定gcThreadTrimNeeded為flase後才能再次執行並行gc,所以並行gc和trimHeaps是交替執行的, trimHeaps就是請求作業系統釋放一些實體記憶體。