1. 程式人生 > >Dalvik虛擬機器垃圾收集(GC)過程分析

Dalvik虛擬機器垃圾收集(GC)過程分析

       前面我們分析了Dalvik虛擬機器堆的建立過程,以及Java物件在堆上的分配過程。這些知識都是理解Dalvik虛擬機器垃圾收集過程的基礎。垃圾收集是一個複雜的過程,它要將那些不再被引用的物件進行回收。一方面要求Dalvik虛擬機器能夠標記出哪些物件是不再被引用的。另一方面要求Dalvik虛擬機器儘快地回收記憶體,避免應用程式長時間停頓。本文就將詳細分析Dalvik虛擬機器是如何解決上述問題完成垃圾收集過程的。

《Android系統原始碼情景分析》一書正在進擊的程式設計師網(http://0xcc0xcd.com)中連載,點選進入!

        Dalvik虛擬機器使用Mark-Sweep演算法來進行垃圾收集。顧名思義,Mark-Sweep演算法就是為Mark和Sweep兩個階段進行垃圾回收。其中,Mark階段從根集(Root Set)開始,遞迴地標記出當前所有被引用的物件,而Sweep階段負責回收那些沒有被引用的物件。在分析Dalvik虛擬機器使用的Mark-Sweep演算法之前,我們先來了解一下什麼情況下會觸發GC。

         Dalvik虛擬機器在三種情況下會觸發四種類型的GC。每一種型別GC使用一個GcSpec結構體來描述,它的定義如下所示:

struct GcSpec {
  /* If true, only the application heap is threatened. */
  bool isPartial;
  /* If true, the trace is run concurrently with the mutator. */
  bool isConcurrent;
  /* Toggles for the soft reference clearing policy. */
  bool doPreserve;
  /* A name for this garbage collection mode. */
  const char *reason;
};
        這個結構體定義在檔案dalvik/vm/alloc/Heap.h中。

        GcSpec結構體的各個成員變數的含義如下所示:

        isPartial: 為true時,表示僅僅回收Active堆的垃圾;為false時,表示同時回收Active堆和Zygote堆的垃圾。

        isConcurrent: 為true時,表示執行並行GC;為false時,表示執行非並行GC。

        doPreserve: 為true時,表示在執行GC的過程中,不回收軟引用引用的物件;為false時,表示在執行GC的過程中,回收軟引用引用的物件。

        reason:

一個描述性的字串。

        Davlik虛擬機器定義了四種類的GC,如下所示:

/* Not enough space for an "ordinary" Object to be allocated. */
extern const GcSpec *GC_FOR_MALLOC;

/* Automatic GC triggered by exceeding a heap occupancy threshold. */
extern const GcSpec *GC_CONCURRENT;

/* Explicit GC via Runtime.gc(), VMRuntime.gc(), or SIGUSR1. */
extern const GcSpec *GC_EXPLICIT;

/* Final attempt to reclaim memory before throwing an OOM. */
extern const GcSpec *GC_BEFORE_OOM;
        這四個全域性變數宣告在檔案dalvik/vm/alloc/Heap.h中。

        它們的含義如下所示:

        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都是在分配物件的過程觸發的。

        在前面Dalvik虛擬機器為新建立物件分配記憶體的過程分析一文,我們提到,Dalvik虛擬機器在Java堆上分配物件的時候,在碰到分配失敗的情況,會嘗試呼叫函式gcForMalloc進行垃圾回收。

        函式gcForMalloc的實現如下所示:

static void gcForMalloc(bool clearSoftReferences)
{
    ......

    const GcSpec *spec = clearSoftReferences ? GC_BEFORE_OOM : GC_FOR_MALLOC;
    dvmCollectGarbageInternal(spec);
}
       這個函式定義在檔案dalvik/vm/alloc/Heap.cpp中。

       引數clearSOftRefereces表示是否要對軟引用引用的物件進行回收。如果要對軟引用引用的物件進行回收,那麼就表明當前記憶體是非常緊張的了,因此,這時候執行的就是GC_BEFORE_OOM型別的GC。否則的話,執行的就是GC_FOR_MALLOC型別的GC。它們都是通過呼叫函式dvmCollectGarbageInternal來執行的。

       在前面Dalvik虛擬機器為新建立物件分配記憶體的過程分析一文,我們也提到,當Dalvik虛擬機器成功地在堆上分配一個物件之後,會檢查一下當前分配的記憶體是否超出一個閥值,如下所示:

void* dvmHeapSourceAlloc(size_t n)  
{  
    ......

    HeapSource *hs = gHs;  
    Heap* heap = hs2heap(hs);  
    if (heap->bytesAllocated + n > hs->softLimit) {  
        ......  
        return NULL;  
    }  
    void* ptr;  
    if (gDvm.lowMemoryMode) {  
        ......  
        ptr = mspace_malloc(heap->msp, n);  
        ......
    } else {  
        ptr = mspace_calloc(heap->msp, 1, n);  
        ...... 
    }  

    countAllocation(heap, ptr);  
    ......  

    if (heap->bytesAllocated > heap->concurrentStartBytes) {  
        ......  
        dvmSignalCond(&gHs->gcThreadCond);  
    }  
    return ptr;  
}
        這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。

        函式dvmHeapSourceAlloc成功地在Active堆上分配到一個物件之後,就會檢查Active堆當前已經分配的記憶體(heap->bytesAllocated)是否大於預設的閥值(heap->concurrentStartBytes)。如果大於,那麼就會通過條件變數gHs->gcThreadCond喚醒GC執行緒進行垃圾回收。預設的閥值(heap->concurrentStartBytes)是一個比指定的堆最小空閒記憶體小128K的數值。也就是說,當堆的空閒內不足時,就會觸發GC_CONCURRENT型別的GC。

        GC執行緒是Dalvik虛擬機器啟動的過程中建立的,它的執行體函式是gcDaemonThread,實現如下所示:

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;
}
        這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。

        GC執行緒平時沒事的時候,就在條件變數gHs->gcThreadCond上進行等待HEAP_TRIM_IDLE_TIME_MS毫秒(5000毫秒)。如果在HEAP_TRIM_IDLE_TIME_MS毫秒內,都沒有得到執行GC的通知,那麼它就呼叫函式trimHeaps對Java堆進行裁剪,以便可以將堆上的一些沒有使用到的記憶體交還給核心。函式trimHeaps的實現可以參考前面Dalvik虛擬機器Java堆建立過程分析一文。否則的話,就會呼叫函式dvmCollectGarbageInternal進行型別為GC_CONCURRENT的GC。

        注意,函式gcDaemonThread在呼叫函式dvmCollectGarbageInternal進行型別為GC_CONCURRENT的GC之前,會先呼叫函式dvmLockHeap來鎖定堆的。等到GC執行完畢,再呼叫函式dvmUnlockHeap來解除對堆的鎖定。這與函式gcForMalloc呼叫函式dvmCollectGarbageInternal進行型別為GC_FOR_MALLOC和GC_CONCURRENT的GC是一樣的。只不過,對堆進行鎖定和解鎖的操作是在呼叫堆疊上的函式dvmMalloc進行的,具體可以參考前面Dalvik虛擬機器為新建立物件分配記憶體的過程分析一文。

        當應用程式呼叫System.gc、VMRuntime.gc介面,或者接收到SIGUSR1訊號時,最終會呼叫到函式dvmCollectGarbage,它的實現如下所示:

void dvmCollectGarbage()
{
    if (gDvm.disableExplicitGc) {
        return;
    }
    dvmLockHeap();
    dvmWaitForConcurrentGcToComplete();
    dvmCollectGarbageInternal(GC_EXPLICIT);
    dvmUnlockHeap();
}
        這個函式定義在檔案dalvik/vm/alloc/Alloc.cpp中。

        如果Davik虛擬機器在啟動的時候,通過-XX:+DisableExplicitGC選項禁用了顯式GC,那麼函式dvmCollectGarbage什麼也不做就返回了。這意味著Dalvik虛擬機器可能會不支援應用程式顯式的GC請求。

       一旦Dalvik虛擬機器支援顯式GC,那麼函式dvmCollectGarbage就會先鎖定堆,並且等待有可能正在執行的GC_CONCURRENT型別的GC完成之後,再呼叫函式dvmCollectGarbageInternal進行型別為GC_EXPLICIT的GC。

        以上就是三種情況下會觸發四種類型的GC。有了這個背景知識之後 ,我們接下來就開始分析Dalvik虛擬機器使用的Mark-Sweep演算法,整個過程如圖1所示:

圖1 Dalvik虛擬機器垃圾收集過程

        Dalvik虛擬機器支援非並行和並行兩種GC。在圖1中,左邊是非並行GC的執行過程,而右邊是並行GC的執行過程。它們的總體流程是相似的,主要差別在於前者在執行的過程中一直是掛起非GC執行緒的,而後者是有條件地掛起非GC執行緒。

        由前面的分析可以知道,無論是並行GC,還是非並行GC,它們都是通過函式dvmCollectGarbageInternal來執行的。函式dvmCollectGarbageInternal的實現如下所示:

void dvmCollectGarbageInternal(const GcSpec* spec)
{
    ......

    if (gcHeap->gcRunning) {
        ......
        return;
    }
    ......

    gcHeap->gcRunning = true;
    ......

    dvmSuspendAllThreads(SUSPEND_FOR_GC); 
    ......

    if (!dvmHeapBeginMarkStep(spec->isPartial)) {
        ......
        dvmAbort();
    }

    dvmHeapMarkRootSet();
    ......

    if (spec->isConcurrent) {
        ......
        dvmClearCardTable();
        dvmUnlockHeap();
        dvmResumeAllThreads(SUSPEND_FOR_GC);
        ......
    }

    dvmHeapScanMarkedObjects();

    if (spec->isConcurrent) {
        ......
        dvmLockHeap();
        ......
        dvmSuspendAllThreads(SUSPEND_FOR_GC);
        ......
        dvmHeapReMarkRootSet();
        ......
        dvmHeapReScanMarkedObjects();
    }

    dvmHeapProcessReferences(&gcHeap->softReferences,
                             spec->doPreserve == false,
                             &gcHeap->weakReferences,
                             &gcHeap->finalizerReferences,
                             &gcHeap->phantomReferences);
    ......

    dvmHeapSweepSystemWeaks();
    ......
   
    dvmHeapSourceSwapBitmaps();
    .......

    if (spec->isConcurrent) {
        dvmUnlockHeap();
        dvmResumeAllThreads(SUSPEND_FOR_GC);
        ......
    }
    dvmHeapSweepUnmarkedObjects(spec->isPartial, spec->isConcurrent,
                                &numObjectsFreed, &numBytesFreed);
    ......
    dvmHeapFinishMarkStep();
    if (spec->isConcurrent) {
        dvmLockHeap();
    }
    ......

    dvmHeapSourceGrowForUtilization();
    ......

    gcHeap->gcRunning = false;
    ......

    if (spec->isConcurrent) {
        ......
        dvmBroadcastCond(&gDvm.gcHeapCond);
    }

    if (!spec->isConcurrent) {
        dvmResumeAllThreads(SUSPEND_FOR_GC);
        ......
    }

    .......
    dvmEnqueueClearedReferences(&gDvm.gcHeap->clearedReferences);
 
    ......
}

        這個函式定義在檔案dalvik/vm/alloc/Heap.cpp中。

        前面提到,在呼叫函式dvmCollectGarbageInternal之前,堆是已經被鎖定了的,因此這時候任何需要堆上分配物件的執行緒都會被掛起。注意,這不會影響到那些不需要在堆上分配物件的執行緒。

        在圖1中顯示的GC流程中,除了第一步的Lock Heap和最後一步的Unlock Heap,都對應於函式dvmCollectGarbageInternal的實現,它的執行邏輯如下所示:

        第1步到第3步用於並行和非並行GC:

        1.  呼叫函式dvmSuspendAllThreads掛起所有的執行緒,以免它們干擾GC。

        2.  呼叫函式dvmHeapBeginMarkStep初始化Mark Stack,並且設定好GC範圍。關於Mark Stack的介紹,可以參考前面Dalvik虛擬機器垃圾收集機制簡要介紹和學習計劃一文。

        3.  呼叫函式dvmHeapMarkRootSet標記根集物件。

        第4到第6步用於並行GC:

        4.  呼叫函式dvmClearCardTable清理Card Table。關於Card Table的介紹,可以參考前面Dalvik虛擬機器垃圾收集機制簡要介紹和學習計劃一文。因為接下來我們將會喚醒第1步掛起的執行緒。並且使用這個Card Table來記錄那些在GC過程中被修改的物件。

        5.  呼叫函式dvmUnlock解鎖堆。這個是針對呼叫函式dvmCollectGarbageInternal執行GC前的堆鎖定操作。

        6.  呼叫函式dvmResumeAllThreads喚醒第1步掛起的執行緒。

        第7步用於並行和非並行GC:

        7.  呼叫函式dvmHeapScanMarkedObjects從第3步獲得的根集物件開始,歸遞標記所有被根集物件引用的物件。

        第8步到第11步用於並行GC:

        8.  呼叫函式dvmLockHeap重新鎖定堆。這個是針對前面第5步的操作。

        9.  呼叫函式dvmSuspendAllThreads重新掛起所有的執行緒。這個是針對前面第6步的操作。

       10. 呼叫函式dvmHeapReMarkRootSet更新根集物件。因為有可能在第4步到第6步的執行過程中,有執行緒建立了新的根集物件。

       11. 呼叫函式dvmHeapReScanMarkedObjects歸遞標記那些在第4步到第6步的執行過程中被修改的物件。這些物件記錄在Card Table中。

       第12步到第14步用於並行和非並行GC:

       12. 呼叫函式dvmHeapProcessReferences處理那些被軟引用(Soft Reference)、弱引用(Weak Reference)和影子引用(Phantom Reference)引用的物件,以及重寫了finalize方法的物件。這些物件都是需要特殊處理的。

       13. 呼叫函式dvmHeapSweepSystemWeaks回收系統內部使用的那些被弱引用引用的物件。

       14. 呼叫函式dvmHeapSourceSwapBitmaps交換Live Bitmap和Mark Bitmap。執行了前面的13步之後,所有還被引用的物件在Mark Bitmap中的bit都被設定為1。而Live Bitmap記錄的是當前GC前還被引用著的物件。通過交換這兩個Bitmap,就可以使得當前GC完成之後,使得Live Bitmap記錄的是下次GC前還被引用著的物件。

       第15步和第16步用於並行GC:

       15. 呼叫函式dvmUnlock解鎖堆。這個是針對前面第8步的操作。

       16. 呼叫函式dvmResumeAllThreads喚醒第9步掛起的執行緒。

       第17步和第18步用於並行和非並行GC:

       17. 呼叫函式dvmHeapSweepUnmarkedObjects回收那些沒有被引用的物件。沒有被引用的物件就是那些在執行第14步之前,在Live Bitmap中的bit設定為1,但是在Mark Bitmap中的bit設定為0的物件。

       18. 呼叫函式dvmHeapFinishMarkStep重置Mark Bitmap以及Mark Stack。這個是針對前面第2步的操作。

       第19步用於並行GC:

       19. 呼叫函式dvmLockHeap重新鎖定堆。這個是針對前面第15步的操作。

       第20步用於並行和非並行GC:

       20. 呼叫函式dvmHeapSourceGrowForUtilization根據設定的堆目標利用率調整堆的大小。

       第21步用於並行GC:     

       21. 呼叫函式dvmBroadcastCond喚醒那些等待GC執行完成再在堆上分配物件的執行緒。

       第22步用於非並行GC:

       22. 呼叫函式dvmResumeAllThreads喚醒第1步掛起的執行緒。

       第23步用到並行和非並行GC:

       23. 呼叫函式dvmEnqueueClearedReferences將那些目標物件已經被回收了的引用物件增加到相應的Java佇列中去,以便應用程式可以知道哪些引用引用的物件已經被回收了。

       以上就是並行和非並行GC的執行總體流程,它們的主要區別在於,前者在GC過程中,有條件地掛起和喚醒非GC執行緒,而後者在執行GC的過程中,一直都是掛起非GC執行緒的。並行GC通過有條件地掛起和喚醒非GC執行緒,就可以使得應用程式獲得更好的響應性。但是我們也應該看到,並行GC需要多執行一次標記根集物件以及遞迴標記那些在GC過程被訪問了的物件的操作。也就是說,並行GC在使用得應用程式獲得更好的響應性的同時,也需要花費更多的CPU資源。

       為了更深入地理解GC的執行過程,接下來我們再詳細分析在上述的23步中涉及到的重要函式。

       1. dvmSuspendAllThreads

       函式dvmSuspendAllThreads用來掛起Dalvik虛擬機器中除當前執行緒之外的其它執行緒,它的實現如下所示:

void dvmSuspendAllThreads(SuspendCause why)
{
    Thread* self = dvmThreadSelf();
    Thread* thread;
    ......

    lockThreadSuspend("susp-all", why);
    ......

    dvmLockThreadList(self);

    lockThreadSuspendCount();
    for (thread = gDvm.threadList; thread != NULL; thread = thread->next) {
        if (thread == self)
            continue;

        /* debugger events don't suspend JDWP thread */
        if ((why == SUSPEND_FOR_DEBUG || why == SUSPEND_FOR_DEBUG_EVENT) &&
            thread->handle == dvmJdwpGetDebugThread(gDvm.jdwpState))
            continue;

        dvmAddToSuspendCounts(thread, 1,
                              (why == SUSPEND_FOR_DEBUG ||
                              why == SUSPEND_FOR_DEBUG_EVENT)
                              ? 1 : 0);
    }
    unlockThreadSuspendCount();

    for (thread = gDvm.threadList; thread != NULL; thread = thread->next) {
        if (thread == self)
            continue;

        /* debugger events don't suspend JDWP thread */
        if ((why == SUSPEND_FOR_DEBUG || why == SUSPEND_FOR_DEBUG_EVENT) &&
            thread->handle == dvmJdwpGetDebugThread(gDvm.jdwpState))
            continue;

        /* wait for the other thread to see the pending suspend */
        waitForThreadSuspend(self, thread);
        ......
    }

    dvmUnlockThreadList();
    unlockThreadSuspend();

    ......
}
        這個函式定義在檔案dalvik/vm/Thread.cpp中。

        在以下三種情況下,當前執行緒需要掛起其它執行緒:

        1. 執行GC。

        2. 偵錯程式請求。

        3. 發生除錯事件,如斷點命中。

        而且,上述三種情況有可能是同時發生的。例如,一個執行緒在分配物件的過程中發生GC的同時,偵錯程式也剛好請求掛起所有執行緒。這時候就需要保證每一個掛起操作都是順序執行的,即要對掛起執行緒的操作進行加鎖。這是通過呼叫函式函式lockThreadSuspend來實現的。

        函式lockThreadSuspend嘗試獲取gDvm._threadSuspendLock鎖。如果獲取失敗,就表明有其它執行緒也正在請求掛起Dalvik虛擬機器中的執行緒,包括當前執行緒。每一個Dalvik虛擬機器執行緒都有一個Suspend Count計數,每當它們都掛起的時候,對應的Suspend Count計數就會加1,而當被喚醒時,對應的Suspend Count計數就會減1。在獲取gDvm._threadSuspendLock鎖失敗的情況下,當前執行緒按照一定的時間間隔檢查自己的Suspend Count,直到自己的Suspend Count等於0,並且能成功獲取gDvm._threadSuspendLock鎖為止。這樣就可以保證每一個掛起Dalvik虛擬機器執行緒的請求都能夠得到順序執行。

        從函式lockThreadSuspend返回之後,就表明當前執行緒可以執行掛起其它執行緒的操作了。它首先要做的第一件事情是遍歷Dalvik虛擬機器執行緒列表,並且呼叫函式dvmAddToSuspendCounts將列表裡面的每一個執行緒對應的Suspend Count都增加1,但是除了當前執行緒之外。注意,當掛起執行緒的請求是偵錯程式發出或者是由除錯事件觸發的時候,Dalvik虛擬機器中的JDWP執行緒是不可以掛起的,因為JDWP執行緒掛起來之後,就沒法除錯了。在這種情況下,也不能將JDWP執行緒對應的Suspend Count都增加1。

        通過呼叫函式dvmJdwpGetDebugThread可以獲得JDWP執行緒控制代碼,用這個控制代碼和Dalvik虛擬機器執行緒列表中的執行緒控制代碼相比,就可以知道當前遍歷的執行緒是否是JDWP執行緒。同時,通過引數why可以知道當掛起執行緒的請求是否是由偵錯程式發出的或者由除錯事件觸發的,

        注意,在增加被掛起執行緒的Suspend Count計數之前,必須要獲取gDvm.threadSuspendCountLock鎖。這個鎖的獲取和釋放可以通過函式lockThreadSuspendCount和unlockThreadSuspendCount完成。

        將被掛起執行緒的Suspend Count計數都增加1之後,接下來就是等待被掛起執行緒自願將自己掛起來了。這是通過函式waitForThreadSuspend來實現。當一個執行緒自願將自己掛起來的時候,會將自己的狀態設定為非執行狀態(THREAD_RUNNING),這樣函式waitForThreadSuspend通過不斷地檢查一個執行緒的狀態是否處於非執行狀態就可以知道它是否已經掛起來了。

        那麼,一個執行緒在什麼情況才會自願將自己掛起來呢?一個執行緒在執行的過程中,會在合適的時候檢查自己的Suspend Count計數。一旦該計數值不等於0,那麼它就知道有執行緒請求掛起自己,因此它就會很配合地將自己的狀態設定為非執行的,並且將自己掛起來。例如,當一個執行緒通過直譯器執行程式碼時,就會週期性地檢查自己的Suspend Count是否等於0。這裡說的週期性,實際上就是碰到IF指令、GOTO指令、SWITCH指令、RETURN指令和THROW指令等時。

       2. dvmHeapBeginMarkStep

       函式dvmHeapBeginMarkStep用來初始化堆的標記範圍和Mark Stack,它的實現如下所示:

bool dvmHeapBeginMarkStep(bool isPartial)
{
    GcMarkContext *ctx = &gDvm.gcHeap->markContext;

    if (!createMarkStack(&ctx->stack)) {
        return false;
    }
    ctx->finger = NULL;
    ctx->immuneLimit = (char*)dvmHeapSourceGetImmuneLimit(isPartial);
    return true;
}
        這個函式定義在檔案dalvik/vm/alloc/MarkSweep.cpp中。

        在標記過程中用到的各種資訊都儲存一個GcMarkContext結構體描述的GC標記上下文ctx中。其中,ctx->stack描述的是Mark Stack,ctx->finger描述的是一個標記指紋,在遞迴標記物件時會用到,ctx->immuneLimit用來限定堆的標記範圍。

        函式dvmHeapBeginMarkStep呼叫另外一個函式createMarkStack來初始化Mark Stack,它的實現如下所示:

static bool createMarkStack(GcMarkStack *stack)
{
    assert(stack != NULL);
    size_t length = dvmHeapSourceGetIdealFootprint() * sizeof(Object*) /
        (sizeof(Object) + HEAP_SOURCE_CHUNK_OVERHEAD);
    madvise(stack->base, length, MADV_NORMAL);
    stack->top = stack->base;
    return true;
}
        這個函式定義在檔案dalvik/vm/alloc/MarkSweep.cpp中。

        函式createMarkStack根據最壞情況來設定Mark Stack的長度,也就是用當前堆的大小除以物件佔用的最小記憶體得到的結果。當前堆的大小可以通過函式dvmHeapSourceGetIdealFootprint來獲得。在堆上分配的物件,都是從Object類繼承下來的,因此,每一個物件佔用的最小記憶體是sizeof(Object)。由於在為物件分配記憶體時,還需要額外的記憶體來儲存管理資訊,例如實際分配給物件的記憶體位元組數。這些額外的管理資訊佔用的記憶體位元組數通過巨集HEAP_SOURCE_CHUNK_OVERHEAD來描述。

        由於Mark Stack實際上是一個Object指標陣列,因此有了上面的資訊之後,我們就可以計算出Mark Stack的長度length。Mark Stack使用的記憶體塊在Dalvik虛擬機器啟動的時候就建立好的,具體參考前面Dalvik虛擬機器Java堆建立過程分析一文,起始地址位於stack->base中。由於這塊記憶體開始的時候被函式madvice設定為MADV_DONTNEED,因此這裡要將它重新設定為MADV_NORMAL,以便可以通知核心,這塊記憶體要開始使用了。

        Mark Stack的棧頂stack->top指向的是Mark Stack記憶體塊的起始位置,以後就會從這個位置開始由小到大增長。

        回到函式dvmHeapBeginMarkStep中,ctx->immuneLimit記錄的實際上是堆的起始標記位置。在此位置之前的物件,都不會被GC。這個位置是通過函式dvmHeapSourceGetImmuneLimit來確定的,它的實現如下所示:

void *dvmHeapSourceGetImmuneLimit(bool isPartial)
{
    if (isPartial) {
        return hs2heap(gHs)->base;
    } else {
        return NULL;
    }
}
        這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。

        當引數isPartial等於true時,函式dvmHeapSourceGetImmuneLimit返回的是Active堆的起始位置,否則的話就返回NULL值。也就是說,如果當前執行的GC只要求回收部分垃圾,那麼就只回收Active堆的垃圾,否則的話,就同時回收Active堆和Zygote堆的垃圾。

        3. dvmHeapMarkRootSet

        函式dvmHeapMarkRootSet用來的標記根集物件,它的實現如下所示:

/* Mark the set of root objects.
 *
 * Things we need to scan:
 * - System classes defined by root classloader
 * - For each thread:
 *   - Interpreted stack, from top to "curFrame"
 *     - Dalvik registers (args + local vars)
 *   - JNI local references
 *   - Automatic VM local references (TrackedAlloc)
 *   - Associated Thread/VMThread object
 *   - ThreadGroups (could track & start with these instead of working
 *     upward from Threads)
 *   - Exception currently being thrown, if present
 * - JNI global references
 * - Interned string table
 * - Primitive classes
 * - Special objects
 *   - gDvm.outOfMemoryObj
 * - Objects in debugger object registry
 *
 * Don't need:
 * - Native stack (for in-progress stuff in the VM)
 *   - The TrackedAlloc stuff watches all native VM references.
 */
void dvmHeapMarkRootSet()
{
    GcHeap *gcHeap = gDvm.gcHeap;
    dvmMarkImmuneObjects(gcHeap->markContext.immuneLimit);
    dvmVisitRoots(rootMarkObjectVisitor, &gcHeap->markContext);
}

        這個函式定義在檔案dalvik/vm/alloc/MarkSweep.cpp中。

        通過註釋我們可以看到根集物件都包含了哪些物件。總的來說,就是包含兩大類物件。一類是Dalvik虛擬機器內部使用的全域性物件,另一類是應用程式正在使用的物件。前者會維護在內部的一些資料結構中,例如Hash表,後者維護在呼叫棧中。對這些根集物件進行標記都是通過函式dvmVisitRoots和回撥函式rootMarkObjectVisitor進行的。

        此外,我們還需要將不在堆回收範圍內的物件也看作是根集物件,以便後面可以使用統一的方法來遍歷這兩類物件所引用的其它物件。標記不在堆回收範圍內的物件是通過函式dvmMarkImmuneObjects來實現的,如下所示:

void dvmMarkImmuneObjects(const char *immuneLimit)
{
    ......

    for (size_t i = 1; i < gHs->numHeaps; ++i) {
        if (gHs->heaps[i].base < immuneLimit) {
            assert(gHs->heaps[i].limit <= immuneLimit);
            /* Compute the number of words to copy in the bitmap. */
            size_t index = HB_OFFSET_TO_INDEX(
                (uintptr_t)gHs->heaps[i].base - gHs->liveBits.base);
            /* Compute the starting offset in the live and mark bits. */
            char *src = (char *)(gHs->liveBits.bits + index);
            char *dst = (char *)(gHs->markBits.bits + index);
            /* Compute the number of bytes of the live bitmap to copy. */
            size_t length = HB_OFFSET_TO_BYTE_INDEX(
                gHs->heaps[i].limit - gHs->heaps[i].base);
            /* Do the copy. */
            memcpy(dst, src, length);
            /* Make sure max points to the address of the highest set bit. */
            if (gHs->markBits.max < (uintptr_t)gHs->heaps[i].limit) {
                gHs->markBits.max = (uintptr_t)gHs->heaps[i].limit;
            }
        }
    }
}
        這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。

        從前面分析的函式dvmHeapSourceGetImmuneLimit可以知道,引數immuneList要麼是等於Zygote堆的最大地址值,要麼是等於NULL。這取決於當前GC要執行的是全部垃圾回收還是部分垃圾回收。

        函式dvmMarkImmuneObjects是在當前GC執行之前呼叫的,這意味著當前存活的物件都標記在Live Bitmap中。現在函式dvmMarkImmuneObjects要做的就是將不在回收範圍內的物件的標記位從Live Bitmap拷貝到Mark Bitmap去。具體做法就是分別遍歷Active堆和Zygote堆,如果它們處於不回範圍中,那麼就對裡面的物件在Live Bitmap中對應的記憶體塊拷貝到Mark Bitmap的對應位置去。

       計算一個物件在一個Bitmap的標記位所在的記憶體塊是通過巨集HB_OFFSET_TO_INDEX和HB_OFFSET_TO_BYTE_INDEX來實現的。這兩個巨集的具體實現可以參考前面Dalvik虛擬機器Java堆建立過程分析一文。

       回到函式dvmHeapMarkRootSet中,我們繼續分析函式dvmVisitRoots的實現,如下所示:

void dvmVisitRoots(RootVisitor *visitor, void *arg)
{
    assert(visitor != NULL);
    visitHashTable(visitor, gDvm.loadedClasses, ROOT_STICKY_CLASS, arg);
    visitPrimitiveTypes(visitor, arg);
    if (gDvm.dbgRegistry != NULL) {
        visitHashTable(visitor, gDvm.dbgRegistry, ROOT_DEBUGGER, arg);
    }
    if (gDvm.literalStrings != NULL) {
        visitHashTable(visitor, gDvm.literalStrings, ROOT_INTERNED_STRING, arg);
    }
    dvmLockMutex(&gDvm.jniGlobalRefLock);
    visitIndirectRefTable(visitor, &gDvm.jniGlobalRefTable, 0, ROOT_JNI_GLOBAL, arg);
    dvmUnlockMutex(&gDvm.jniGlobalRefLock);
    dvmLockMutex(&gDvm.jniPinRefLock);
    visitReferenceTable(visitor, &gDvm.jniPinRefTable, 0, ROOT_VM_INTERNAL, arg);
    dvmUnlockMutex(&gDvm.jniPinRefLock);
    visitThreads(visitor, arg);
    (*visitor)(&gDvm.outOfMemoryObj, 0, ROOT_VM_INTERNAL, arg);
    (*visitor)(&gDvm.internalErrorObj, 0, ROOT_VM_INTERNAL, arg);
    (*visitor)(&gDvm.noClassDefFoundErrorObj, 0, ROOT_VM_INTERNAL, arg);
}
       這個函式定義在檔案dalvik/vm/alloc/Visit.cpp。

       引數visitor指向的是函式rootMarkObjectVisitor,它負責將根集物件在Mark Bitmap中的位設定為1。我們後面再分析這個函式的實現。

       以下物件將被視為根集物件:

       1. 被載入到Dalvik虛擬機器的類物件。這些類物件快取在gDvm.loadedClasses指向的一個Hash表中,通過函式visitHashTable來遍歷和標記。

       2. 在Dalvik虛擬機器內部建立的原子類,例如Double、Boolean類等,通過函式visitPrimitiveTypes來遍歷和標記。

       3. 註冊在偵錯程式的物件。這些物件儲存在gDvm.dbgRegistry指向的一個Hash表中,通過函式visitHashTable來遍歷和標記。

       4. 字串常量池中的String物件。這些物件儲存儲存在gDvm.literalStrings指向的一個Hash表中,通過函式visitHashTable來遍歷和標記。

       5. 在JNI中建立的全域性引用物件所引用的物件。這些被引用物件儲存在gDvm.jniGlobalRefTable指向的一個間接引用表中,通過函式visitIndirectRefTable來遍歷和標記。

       6. 在JNI中通過GetStringUTFChars、GetByteArrayElements等介面訪問字串或者陣列時被Pin住的物件。這些物件儲存在gDvm.jniPinRefTable指向的一個引用表中,通過函式visitReferenceTable來遍歷和標記。

       7. 當前位於Davlik虛擬機器執行緒的呼叫棧的物件。這些物件記錄在棧幀中,通過函式visitThreads來遍歷和標記。

       8. 在Dalvik虛擬機器內部建立的OutOfMemory異常物件,這個物件儲存在gDvm.outOfMemoryObj中。

       9. 在Dalvik虛擬機器內部建立的InternalError異常物件,這個物件儲存在gDvm.internalErrorObj中。

       10. 在Dalvik虛擬機器內部建立的N oClassDefFoundError異常物件,這個物件儲存在gDvm.noClassDefFoundErrorObj中。

       在上述這些根集物件中,最複雜和最難遍歷和標記的就是位於Dalvik虛擬機器執行緒棧中的物件,因此接下來我們就繼續分析函式發isitThreads的實現,如下所示:

static void visitThreads(RootVisitor *visitor, void *arg)
{
    Thread *thread;

    assert(visitor != NULL);
    dvmLockThreadList(dvmThreadSelf());
    thread = gDvm.threadList;
    while (thread) {
        visitThread(visitor, thread, arg);
        thread = thread->next;
    }
    dvmUnlockThreadList();
}
        這個函式定義在檔案dalvik/vm/alloc/Visit.cpp。

        所有的Dalvik虛擬機器執行緒都儲存在gDvm.threadList指向的一個列表中,函式visitThreads通過呼叫另外一個函式函式visitThread來遍歷和標記每一個Dalvik虛擬機器棧上物件。後者的實現如下所示:

static void visitThread(RootVisitor *visitor, Thread *thread, void *arg)
{
    u4 threadId;

    assert(visitor != NULL);
    assert(thread != NULL);
    threadId = thread->threadId;
    (*visitor)(&thread->threadObj, threadId, ROOT_THREAD_OBJECT, arg);
    (*visitor)(&thread->exception, threadId, ROOT_NATIVE_STACK, arg);
    visitReferenceTable(visitor, &thread->internalLocalRefTable, threadId, ROOT_NATIVE_STACK, arg);
    visitIndirectRefTable(visitor, &thread->jniLocalRefTable, threadId, ROOT_JNI_LOCAL, arg);
    if (thread->jniMonitorRefTable.table != NULL) {
        visitReferenceTable(visitor, &thread->jniMonitorRefTable, threadId, ROOT_JNI_MONITOR, arg);
    }
    visitThreadStack(visitor, thread, arg);
}
        這個函式定義在檔案dalvik/vm/alloc/Visit.cpp。

        對於每一個Dalvik虛擬機器執行緒來說,除了它當前棧上的物件需要標記為根集物件之外,還有以下物件需要標記為根集物件:

        1. 用來描述Dalvik虛擬機器執行緒的執行緒物件。這個物件儲存在thread->threadObj中。

        2. 當前Davik虛擬機器執行緒的未處理異常物件。這個物件保在在thread->expception中。

        3. Dalvik虛擬機器執行緒內部建立的物件,例如執行過程中丟擲的異常物件。這些物件儲存在thread->internalLocalRefTable指向的一個引用表中。通過函式visitReferenceTable來遍歷和標記。

        4. Dalvik虛擬機器執行緒在JNI中建立的區域性引用物件。這些物件儲存在thread->jniLocalRefTable指向的一個間接引用表中,通過函式visitIndirectRefTable來遍歷和標記。

        5. Dalvik虛擬機器執行緒在JNI中建立的用於同步的Monitor物件。這些物件儲存在thread->jniMonitorRefTable指向的一個引用表中,通過函式visitReferenceTable來遍歷和標記。

       當前棧上的物件通過函式visitThreadStack來遍歷和標記,它的實現如下所示:

static void visitThreadStack(RootVisitor *visitor, Thread *thread, void *arg)
{
    ......
    u4 threadId = thread->threadId;
    const StackSaveArea *saveArea;
    for (u4 *fp = (u4 *)thread->interpSave.curFrame;
         fp != NULL;
         fp = (u4 *)saveArea->prevFrame) {
        Method *method;
        saveArea = SAVEAREA_FROM_FP(fp);
        method = (Method *)saveArea->method;
        if (method != NULL && !dvmIsNativeMethod(method)) {
            const RegisterMap* pMap = dvmGetExpandedRegisterMap(method);
            const u1* regVector = NULL;
            if (pMap != NULL) {
                ......
                int addr = saveArea->xtra.currentPc - method->insns;
                regVector = dvmRegisterMapGetLine(pMap, addr);
            }
            if (regVector == NULL) {
                ......
                for (size_t i = 0; i < method->registersSize; ++i) {
                    if (dvmIsValidObject((Object *)fp[i])) {
                        (*visitor)(&fp[i], threadId, ROOT_JAVA_FRAME, arg);
                    }
                }
            } else {
                ......
                u2 bits = 1 << 1;
                for (size_t i = 0; i < method->registersSize; ++i) {
                    bits >>= 1;
                    if (bits == 1) {
                        .......
                        bits = *regVector++ | 0x0100;
                    }
                    if ((bits & 0x1) != 0) {
                        ......
                        (*visitor)(&fp[i], threadId, ROOT_JAVA_FRAME, arg);
                    }
                }
                dvmReleaseRegisterMapLine(pMap, regVector);
            }
        }
        ......
    }
}

        這個函式定義在檔案dalvik/vm/alloc/Visit.cpp。

        在Dalvik虛擬機器中,每一個Dalvik虛擬機器執行緒都用一個Thread結構體來描述。在Thread結構體中,有一個成員變數interpSave,它指向的是一個InterpSaveState結構體。在InterpSaveState結構體中,又有一個成員變數curFrame,它指向執行緒的當前呼叫棧幀,如圖2所示:


圖2 Dalvik虛擬機器執行緒的呼叫棧

       在圖2中,Dalvik虛擬機器呼叫棧由高地址往低地址增長,即棧頂位於低地址。在每一個呼叫幀前面,有一個型別為StackSaveArea的結構體。在StackSaveArea結構體中,有四個重要的成員變數prevFrame、savedPc、method和currentPc。其中,prevFrame指向前一個呼叫幀,savedPc指向當前函式呼叫完成後的返回地址,method指向當前呼叫的函式,currentPc指向當前執行的指令。通過成員變數prevFrame,就可以從當前呼叫棧幀開始,遍歷整個呼叫棧。此外,通過成員變數method,可以知道一個呼叫棧幀對應的呼叫函式是一個Java函式還是一個JNI函式。如果是一個JNI函式,那麼是不需要檢查它的棧幀的。因為JNI函式不會在裡面儲存Java物件。

       那麼在JNI函式裡面建立或者訪問的物件是如何管理的呢?我們在JNI函式中需要建立或者訪問Java物件時,都必須要通過JNI介面來進行。JNI介面會把JNI函式建立或者訪問的Java物件儲存執行緒相關的一個JNI Local Reference Table中。這些被JNI Local Reference Table引用的Java物件,在JNI函式呼叫結束的時候,會相應地被自動釋放引用。但是有些情況下,我們需要在JNI函式中手動釋放被JNI Local Reference Table引用的Java物件,因為JNI Local Reference Table的大小是有限的。一旦一個JNI函式引用的Java物件數大於JNI Local Reference Table的容量,那麼就會發生錯誤。這種情況特別容易發生迴圈語句中。

       由於JNI函式裡面建立或者訪問的物件儲存在JNI Local Reference Table中,因此,在前面分析的函式visitThread中,需要對每一個Dalvik虛擬機器執行緒的JNI Local Reference Table中的Java物件進行遍歷和標記,避免它們在GC過程中被回收。

       好了,我們現在將目光返回到Dalvik虛擬機器執行緒的呼叫棧中,我們需要做的是遍歷那些非JNI函式呼叫,並且對位於棧幀裡面的Java物件進行標記,以免它們在GC過程中被回收。我們知道,Dalvik虛擬機器指令是基於暫存器的。也就是說,無論函式裡面的引數,還是區域性變數,都是儲存在暫存器中。但是,我們需要注意的是。這些暫存器不是真實的硬體暫存器,而是虛擬出來的。那麼是怎麼虛擬出來的呢?其實就是將暫存器對映到呼叫棧幀對就的記憶體塊上。因此,我們只需要遍歷呼叫棧幀對應的記憶體塊,就可以知道執行緒的呼叫棧都引用了哪些Java物件,從而可以對它們進行標記。

       現在,新的問題又出現了。呼叫棧幀的資料不一定都是Java物件引用,還有可能是一個原子型別,例如整數、布林變數或者浮點數。在遍歷呼叫棧幀的時候,我們是怎麼知道里面的一個數據到底是Java物件引用還是一個原子型別呢?我們可以想到的一個辦法是判斷它們值。如果它們的值是位於Java堆的範圍內,那麼就認為它們是Java物件引用。但是這樣做是不準確的,因為有可能這是一個整數值,並且這個整數值剛好落在Java堆的地址範圍之內。不過,我們寧願將一個整數值誤認為一個Java物件引用,也比漏掉一個Java物件引用好。因為將一個整數值誤認為一個Java物件引用,導致的後果僅僅是垃圾不能及時回收,而漏掉一個Java物件引用,則意味著一個正在使用Java物件還在被引用的時候被回收。

       上面描述的在呼叫棧幀中,大小值只要是落在Java堆的地址範圍之內就認為是Java物件引用的做法稱為保守GC演算法,與此相對的稱為準確GC。在準確GC中,用一個稱為Register Map的資料結構來輔助GC。Register Map記錄了一個Java函式所有可能的GC執行點所對應的暫存器使用情況。如果在某一個GC執行點,某一個暫存器對應的是一個Java物件引用,那麼在對應的Register Map中,就有一位被標記為1。

       從前面我們分析的函式dvmSuspendAllThreads可以知道,每當一個Dalvik虛擬機器執行緒被掛起等待GC時,它們總是掛起在IF、GOTO、SWITCH、RETURN和THROW等跳轉指令中。這些指令點對應的實際上就GC執行點。因此,我們可以在一個函式中針對出現上述指令的地方,均記錄函式當前的暫存器使用情況,從而可以實現準確GC。那麼,這個工作是由誰去做的呢?我們知道,APK在安裝的時候,安裝服務會它們的DEX位元組碼進行優化,得到一個odex檔案。實際上,APK在安裝的時候,它們的DEX位元組碼除了會被優化之外,還會被驗證和分析。驗證是為了保證不包含非法指令,而分析就是為了得到指令的執行狀況,其中就包括得到每一個GC執行點的暫存器使用情況,最終形成一個Register Map,並且儲存在odex檔案中。這樣,當一個odex檔案被載入使用的時候,我們就可以直接獲得一個函式在某一個GC執行點的暫存器使用情況。

       函式visitThreadStack在遍歷呼叫棧幀的過程中,通過呼叫函式dvmGetExpandedRegisterMap可以獲得當前呼叫函式對應的Register Map。有了這個Register Map之後,再通過在當前StackSaveArea結構體的成員變數currentPc的值,就可以獲得一個用來描述呼叫函式當前暫存器使用情況的一個暫存器向量regVector。在獲得的暫存器向量regVector中,如果某一個位的值等於1,那麼就說明對應的暫存器儲存的是一個Java物件引用。在這種情況下,就需要將該Java物件標記為活的。

       注意,函式visitThreadStack按照位元組來訪問暫存器向量regVector。每次讀出1個位元組的資料到變數bits的0~7位,然後再將變數bits的第8位設定為1。此後從低位開始,每訪問一位就將變數bits向右移一位。當向左移夠8位後,就能保證變數bits的值變為1,這時候就再從暫存器向量regVector讀取下一位元組進行遍歷。

       由於Register Map不是強制的,因此有可能某些函式不存在對應的Register Map,在這種情況下,就需要使用前面我們所分析的保守GC演算法,遍歷呼叫棧幀的所有資料,只要它們的值在Java堆的範圍之內,均認為它們是Java物件引用,並且對它們進行標記。

       在前面分析的一系列函式,都是通過呼叫函式rootMarkObjectVisitor來對物件進行標記的,它的實現如下所示:

static void rootMarkObjectVisitor(void *addr, u4 thread, RootType type,
                                  void *arg)
{
    ......
    Object *obj = *(Object **)addr;
    GcMarkContext *ctx = (GcMarkContext *)arg;
    if (obj != NULL) {
        markObjectNonNull(obj, ctx, false);
    }
}
        這個函式定義在檔案dalvik/vm/alloc/MarkSweep.cpp中。

        引數addr指向的是Java物件地址,arg指向一個描述當前GC標記上下文的GcMarkContext結構體。函式rootMarkObjectVisitor通過呼叫另外一個函式markObjectNonNull來標記引數addr所描述的Java物件。

        函式markObjectNonNull的實現如下所示:

static void markObjectNonNull(const Object *obj, GcMarkContext *ctx,
                              bool checkFinger)
{
    ......
    if (obj < (Object *)ctx->immuneLimit) {
        assert(isMarked(obj, ctx));
        return;
    }
    if (!setAndReturnMarkBit(ctx, obj)) {
        /* This object was not previously marked.
         */
        if (checkFinger && (void *)obj < ctx->finger) {
            /* This object will need to go on the mark stack.
             */
            markStackPush(&ctx->stack, obj);
        }
    }
}
        這個函式定義在檔案dalvik/vm/alloc/MarkSweep.cpp中。

        函式markObjectNonNull不僅在標記根集物件的時候會呼叫到,在遞迴標記被根集物件所引用的物件的時候也會呼叫到。在標記根集物件呼叫時,引數checkFlinger的值等於false,而在遞迴標記被根集物件所引用的物件時,引數checkFlinger的值等於true,並且引數ctx指向的GcMarkContext結構體的成員變數finger被設定為當前遍歷過的Java物件的最大地址值。

        前面提到,對於在非回收範圍內的Java物件,它們一開始的時候就已經標記為存活的了,因此,函式markObjectNonNull一開始就判斷一個引數obj描述的Java物件是否在非回收堆範圍內。如果是的話,那麼就不需要重複對其它進行標記了。

        對於在回收範圍內的Java物件,則需要呼叫函式setAndReturnMarkBit將其在Mark Bitmap中對應的位設定為1。函式setAndReturnMarkBit在將一個位設定為1之前,要返回該位的舊值。也就是說,當函式setAndReturnMarkBit的返回值等於0時,就表示一個Java物件是第一次被標記。在這種情況下,如果引數checkFlinger的值等於true,並且被遍歷的Java物件的地址值小於引數ctx指向的GcMarkContext結構體的成員變數finger,那麼就需要呼叫函式markStackPush將該物件壓入Mark Stack中,以便後面可以繼續對該物件進行遞迴遍歷。

       函式setAndReturnMarkBit的實現如下所示:

static long setAndReturnMarkBit(GcMarkContext *ctx, const void *obj)
{
    return dvmHeapBitmapSetAndReturnObjectBit(ctx->bitmap, obj);
}
        這個函式定義在檔案dalvik/vm/alloc/MarkSweep.cpp中。

        從前面Dalvik虛擬機器Java堆建立過程分析一文可以知道,ctx->bitmap指向的是Mark Bitmap,因此函式setAndReturnMarkBit是將引數obj描述的Java物件在Mark Bitmap中對應的位設定為1。

        函式markStackPush的實現如下所示:

static void markStackPush(GcMarkStack *stack, const Object *obj)
{
    ......
    *stack->top = obj;
    ++stack->top;
}
        這個函式定義在檔案dalvik/vm/alloc/MarkSweep.cpp中。

        從這裡就可以看出,函式markStackPush將引數obj描述的Java物件壓入到Mark Stack中。凡是在Mark Stack中的Java物件,都是需要遞迴遍歷它們所引用的Java物件的。

        這樣,當函式dvmHeapMarkRootSet呼叫完畢,所有的根集物件在Mark Bitmap中對應的位就都被設定為1了。

        4.  dvmClearCardTable

        函式dvmClearCardTable用來清零Card Table,它的實現如下所示:

void dvmClearCardTable()
{
    ......

    if (gDvm.lowMemoryMode) {
      // zero out cards with madvise(), discarding all pages in the card table
      madvise(gDvm.gcHeap->cardTableBase, gDvm.gcHeap->cardTableLength, MADV_DONTNEED);
    } else {
      // zero out cards with memset(), using liveBits as an estimate
      const HeapBitmap* liveBits = dvmHeapSourceGetLiveBits();
      size_t maxLiveCard = (liveBits->max - liveBits->base) / GC_CARD_SIZE;
      maxLiveCard = ALIGN_UP_TO_PAGE_SIZE(maxLiveCard);
      if (maxLiveCard > gDvm.gcHeap->cardTableLength) {
          maxLiveCard = gDvm.gcHeap->cardTableLength;
      }

      memset(gDvm.gcHeap->cardTableBase, GC_CARD_CLEAN, maxLiveCard);
    }
}

        這個函式定義在檔案dalvik/vm/alloc/CardTable.cpp中。

        在低記憶體模式下,函式dvmClearCardTable呼叫另外一個函式madvice將Card Table對應的虛擬記憶體塊標記為MADV_DONTNEED,以便這塊虛擬記憶體對應的實體記憶體頁可以被核心回收。但是當Card Table對應的虛擬記憶體塊被訪問的時候,核心會重新給它對映物理頁,並且會將被對映的物理頁的值初始化為0。通過這種巧妙的方式,不僅可以將Card Table清零,而且還可以釋放暫時不使用的記憶體,符合低記憶體模式執行的要求。不過我們也應該注意到,這種做法實際上是通過犧牲程式效能來換取空間的,因為重新給一塊虛擬記憶體對映物理頁,以及對該物理頁進行初始化都是需要花費時間的。在計算機的世界時,根據實際需要,以時間換空間,或者以空間換時間,都是經典的做法。

        在非低記憶體模式下,函式dvmClearCardTable並沒有對整個Card Table進行清零。從前面Dalvik虛擬機器垃圾收集機制簡要介紹和學習計劃一文可以知道,Card Table雖然只有Heap Bitmap一半的大小,但是當堆較大時,Card Table的絕對值仍然是不小的。而且每次執行GC時,整個堆並沒有完全用完。因此,函式dvmClearCardTable就根據當前存活的地址值最大的Java物件來評估應該清零的Card Table大小。一來是因為只有當前存活的Java物件才可能在並行GC期間訪問其引用的其它Java物件,二來是不清零部分不會對程式產生影響。試想未清零部分的Card Table,如果它的值本來就於等於0,那麼就已經是我們想要的結果了。另一方面,如果未清零部分的Card Table的值等於1,那麼產生的後果僅僅就是造成一些垃圾物件沒有及時回收而已,但是不會影響程式的正常邏輯。計算出應該清零的部分Card Table大小之後,就呼叫函式memset來將它們的值設定為GC_CARD_CLEAN,即設定為0。

        5. dvmResumeAllThreads

        函式dvmResumeAllThreads用來喚醒之前被掛起的執行緒,它的實現如下所示:

void dvmResumeAllThreads(SuspendCause why)
{
    Thread* self = dvmThreadSelf();
    Thread* thread;

    lockThreadSuspend("res-all", why);  /* one suspend/resume at a time */
    ......

    dvmLockThreadList(self);
    lockThreadSuspendCount();
    for (thread = gDvm.threadList; thread != NULL; thread = thread->next) {
        if (thread == self)
            continue;

        /* debugger events don't suspend JDWP thread */
        if ((why == SUSPEND_FOR_DEBUG || why == SUSPEND_FOR_DEBUG_EVENT) &&
            thread->handle == dvmJdwpGetDebugThread(gDvm.jdwpState))
        {
            continue;
        }

        if (thread->suspendCount > 0) {
            dvmAddToSuspendCounts(thread, -1,
                                  (why == SUSPEND_FOR_DEBUG ||
                                  why == SUSPEND_FOR_DEBUG_EVENT)
                                  ? -1 : 0);
        } else {
            LOG_THREAD("threadid=%d:  suspendCount already zero",
                thread->threadId);
        }
    }
    unlockThreadSuspendCount();
    dvmUnlockThreadList();
    ......

    unlockThreadSuspend();
    ......
    
    lockThreadSuspendCount();
    int cc = pthread_cond_broadcast(&gDvm.threadSuspendCountCond);
    ......

    unlockThreadSuspendCount();
    ......
} 
        這個函式定義在檔案alvik/vm/Thread.cpp中。

        對照我們前面分析的函式dvmSuspendAllThreads的實現,就可以比較容易理解函式dvmResumeAllThreads的實現了。在GC期間,GC執行緒需要掛起其它的Dalvik虛擬機器執行緒時,就將它們的Suspend Count增加1。各個Dalvik虛擬機器執行緒在執行的過程中,會週期性地檢查自己的Suspend Count。一旦發現自己的Suspend Count大於0,那麼就會將自己掛起,等待GC執行緒喚醒。

        我們以GOTO指令的執行過程說明一個Dalvik虛擬機器自願掛起自己的過程。通過前面Dalvik虛擬機器的執行過程分析一文的學習,我們很容易找到GOTO指令的解釋執行過程,如下所示:

HANDLE_OPCODE(OP_GOTO /*+AA*/)
    vdst = INST_AA(inst);
    if ((s1)vdst < 0)
        ILOGV("|goto -0x%02x", -((s1)vdst));
    else
        ILOGV("|goto +0x%02x", ((s1)vdst));
    ILOGV("> branch taken");
    if ((s1)vdst < 0)
        PERIODIC_CHECKS((s1)vdst);
    FINISH((s1)vdst);
OP_END
       這段程式碼定義在檔案dalvik/vm/mterp/out/InterpC-portable.cpp中。

       如果是向後跳轉,那麼就會呼叫巨集PERIODIC_CHECKS來檢查是否需要掛起當前執行緒。

       巨集PERIODIC_CHECKS的定義如下所示:

#define PERIODIC_CHECKS(_pcadj) {                              \
        if (dvmCheckSuspendQuick(self)) {                                   \
            EXPORT_PC();  /* need for precise GC */                         \
            dvmCheckSuspendPending(self);                                   \
        }                                                                   \
    }
       這個巨集定義在檔案dalvik/vm/mterp/out/InterpC-portable.cpp中。

       巨集PERIODIC_CHECKS首先是呼叫函式dvmCheckSuspendQuick來檢查儲存在當前執行緒物件裡面的一個kSubModeSuspendPending標誌位是否等於1。當這個kSubModeSuspendPending等於1時候,就說明執行緒的Suspend Count值大於0。換句話說,每當一個執行緒的Suspend Count增加1之後的值大於0,那麼都會相應地將對應的執行緒對物件的kSubModeSuspendPending標誌位設定為1。以便後面可以用來快速檢測執行緒是否有掛起請求。

       在當前執行緒的Suspend Count大於0的情況下,巨集PERIODIC_CHECKS接下來做兩件事情。第一件事情是呼叫巨集EXPORT_PC將當前的PC值記錄在當前呼叫棧幀的StackSaveArea結構體中的成員變數currentPc中,以便可以執行準確GC。這一點是我們前面分析過的。第二件事情就是呼叫函式dvmCheckSuspendPending來將自己掛起。

       函式dvmCheckSuspendPending的實現如下所示:

bool dvmCheckSuspendPending(Thread* self)
{
    assert(self != NULL);
    if (self->suspendCount == 0) {
        return false;
    } else {
        return fullSuspendCheck(self);
    }
}
       這個函式定義在檔案dalvik/vm/Thread.cpp中。

       在我們這個情景中,當前執行緒的Suspend Count肯定是不等於0的,因此,接下來就會呼叫函式fullSuspendCheck。

       函式fullSuspendCheck的實現如下所示:

static bool fullSuspendCheck(Thread* self)
{
    ......
    lockThreadSuspendCount();   /* grab gDvm.threadSuspendCountLock */

    bool needSuspend = (self->suspendCount != 0);
    if (needSuspend) {
        ......
        ThreadStatus oldStatus = self->status;      /* should be RUNNING */
        self->status = THREAD_SUSPENDED;
        ......

        while (self->suspendCount != 0) {
            ......
            dvmWaitCond(&gDvm.threadSuspendCountCond,
                    &gDvm.threadSuspendCountLock);
        }
        ......
        self->status = oldStatus;
        ......
    }

    unlockThreadSuspendCount();

    return needSuspend;
}
        這個函式定義在檔案dalvik/vm/Thread.cpp中。

        從這裡就可以清楚地看到,函式fullSuspendCheck在當前執行緒的Supend Count不等於0的情況下,首先是將自己的狀態設定為THREAD_SUSPENDED,接著掛起在條件變數gDvm.threadSuspendCountCond上,直到被喚醒為止,然後再恢復為之前的狀態。

        有了這些背景知識之後,回到函式dvmResumeAllThreads中,我們就更容易理解它的實現了。它所要做的就是將每一個被掛起的Dalvik虛擬機器執行緒的Suspend Count減少1,並且在最後將掛起在條件變數gDvm.threadSuspendCountCond上的所有執行緒都喚醒。

        6. dvmHeapScanMarkedObjects

        函式dvmHeapScanMarkedObjects用來遞迴標記根集物件所引用的其它物件,它的實現如下所示:

void dvmHeapScanMarkedObjects(void)
{
    GcMarkContext *ctx = &gDvm.gcHeap->markContext;

    ......
    dvmHeapBitmapScanWalk(ctx->bitmap, scanBitmapCallback, ctx);

    ctx->finger = (void *)ULONG_MAX;

    ......
    processMarkStack(ctx);
}

        這個函式定義在檔案dalvik/vm/alloc/MarkSweep.cpp中。

        前面已經將根集物件標記在ctx->bitmap指向的Mark Bitmap了,現在就開始呼叫函式dvmHeapBitmapScanWalk來標記它們直接引用的物件。

        函式dvmHeapBitmapScanWalk的實現如下所示:

void dvmHeapBitmapScanWalk(HeapBitmap *bitmap,
                           BitmapScanCallback *callback, void *arg)
{
    ......
    uintptr_t end = HB_OFFSET_TO_INDEX(bitmap->max - bitmap->base);
    uintptr_t i;
    for (i = 0; i <= end; ++i) {
        unsigned long word = bitmap->bits[i];
        if (UNLIKELY(word != 0)) {
            unsigned long highBit = 1 << (HB_BITS_PER_WORD - 1);
            uintptr_t ptrBase = HB_INDEX_TO_OFFSET(i) + bitmap->base;
            void *finger = (void *)(HB_INDEX_TO_OFFSET(i + 1) + bitmap->base);
            while (word != 0) {
                const int shift = CLZ(word);
                Object *obj = (Object *)(ptrBase + shift * HB_OBJECT_ALIGNMENT);
                (*callback)(obj, finger, arg);
                word &= ~(highBit >> shift);
            }
            end = HB_OFFSET_TO_INDEX(bitmap->max - bitmap->base);
        }
    }
}
         這個函式定義在檔案dalvik/vm/alloc/MarkSweep.cpp中。

         函式dvmHeapBitmapScanWalk遍歷Mark Bitmap中值等於1的位,並且呼叫參