記憶體碎片問題其實是一個通用的問題,不單止Dalvik虛擬機器在Java堆為物件分配記憶體時會遇到,C庫的malloc函式在分配記憶體時也會遇到。Android系統使用的C庫bionic使用了Doug Lea寫的dlmalloc記憶體分配器。也就是說,我們呼叫函式malloc的時候,使用的是dlmalloc記憶體分配器來分配記憶體。這是一個成熟的記憶體分配器,可以很好地解決記憶體碎片問題。關於dlmalloc記憶體分配器的設計,可以參考這篇文章:A Memory Allocator。
前面Dalvik虛擬機器垃圾收集機制簡要介紹和學習計劃一文提到,Dalvik虛擬機器的Java堆的底層實現是一塊匿名共享記憶體,並且將其抽象為C庫的一個mspace,如圖1所示:
圖1 Dalvik虛擬機器Java堆
於是,Dalvik虛擬機器就很機智地利用C庫裡面的dlmalloc記憶體分配器來解決記憶體碎片問題!
為了應對可能面臨的記憶體不足問題,Dalvik虛擬機器採用一種漸進的方法來為物件分配記憶體,直到盡了最大努力,如圖2所示:
圖2 Dalvik虛擬機器為物件分配記憶體的過程
接下來,我們就詳細分析這個過程,以便可以瞭解Dalvik虛擬機器是如何解決記憶體不足問題的,以及分配出來的記憶體是如何管理的。
Dalvik虛擬機器實現了一個dvmAllocObject函式。每當Dalvik虛擬機器需要為物件分配記憶體時,就會呼叫函式dvmAllocObject。例如,當Dalvik虛擬機器的直譯器遇到一個new指令時,它就會呼叫函式dvmAllocObject,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
HANDLE_OPCODE(OP_NEW_INSTANCE /*vAA, class@BBBB*/) { ClassObject* clazz; Object* newObj; EXPORT_PC(); vdst = INST_AA(inst); ref = FETCH(1); ...... clazz = dvmDexGetResolvedClass(methodClassDex, ref); if (clazz == NULL) { clazz = dvmResolveClass(curMethod->clazz, ref, false); ...... } ...... newObj = dvmAllocObject(clazz, ALLOC_DONT_TRACK); ...... SET_REGISTER(vdst, (u4) newObj); } FINISH(2); OP_END |
這個程式碼段定義在檔案dalvik/vm/mterp/out/InterpC-portable.cpp中。
關於Dalvik虛擬機器的直譯器的實現,可以參考Dalvik虛擬機器的執行過程分析一文,上面這段程式碼首先是找到要建立的物件的型別clazz,接著以其作為引數呼叫函式dvmAllocObject為要建立的物件分配記憶體。另外一個引數ALLOC_DONT_TRACK是告訴Dalvik虛擬機器的堆管理器,要分配的物件是一個根集物件,不需要對它進行跟蹤。因為根集物件在GC時是會自動被追蹤處理的。
函式dvmAllocObject的實現如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Object* dvmAllocObject(ClassObject* clazz, int flags) { Object* newObj; assert(clazz != NULL); assert(dvmIsClassInitialized(clazz) || dvmIsClassInitializing(clazz)); /* allocate on GC heap; memory is zeroed out */ newObj = (Object*)dvmMalloc(clazz->objectSize, flags); if (newObj != NULL) { DVM_OBJECT_INIT(newObj, clazz); dvmTrackAllocation(clazz, clazz->objectSize); /* notify DDMS */ } return newObj; } |
這個函式定義在檔案dalvik/vm/alloc/Alloc.cpp中。
函式dvmAllocObject呼叫函式dvmMalloc從Java堆中分配一塊指定大小的記憶體給新建立的物件使用。如果分配成功,那麼接下來就先使用巨集DVM_OBJECT_INIT來初始化新建立對物件的成員變數clazz,使得新建立的物件可以與某個特定的類關聯起來,接著再呼叫函式dvmTrackAllocation記錄當前的記憶體分配資訊,以便通知DDMS。
函式dvmMalloc返回的只是一塊記憶體地址,這是沒有型別的。但是由於每一個Java物件都是從Object類繼承下來的,因此,函式dvmAllocObject可以將獲得的沒有型別的記憶體塊強制轉換為一個Object物件。
Object類的定義如下所示:
1 2 3 4 5 6 7 8 9 10 |
struct Object { /* ptr to class object */ ClassObject* clazz; /* * A word containing either a "thin" lock or a "fat" monitor. See * the comments in Sync.c for a description of its layout. */ u4 lock; }; |
這個類定義在檔案dalvik/vm/oo/Object.h中。
Object類有兩個成員變數:clazz和lock。其中,成員變數clazz的型別為ClassObject,它對應於Java層的java.lang.Class類,用來描述物件所屬的類。成員變數lock是一個鎖,正是因為有了這個成員變數,在Java層中,每一個物件都可以當鎖使用。
理解了Object類的定義之後,我們繼續分析函式dvmMalloc的實現,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
void* dvmMalloc(size_t size, int flags) { void *ptr; dvmLockHeap(); /* Try as hard as possible to allocate some memory. */ ptr = tryMalloc(size); if (ptr != NULL) { /* We've got the memory. */ if (gDvm.allocProf.enabled) { Thread* self = dvmThreadSelf(); gDvm.allocProf.allocCount++; gDvm.allocProf.allocSize += size; if (self != NULL) { self->allocProf.allocCount++; self->allocProf.allocSize += size; } } } else { /* The allocation failed. */ if (gDvm.allocProf.enabled) { Thread* self = dvmThreadSelf(); gDvm.allocProf.failedAllocCount++; gDvm.allocProf.failedAllocSize += size; if (self != NULL) { self->allocProf.failedAllocCount++; self->allocProf.failedAllocSize += size; } } } dvmUnlockHeap(); if (ptr != NULL) { /* * If caller hasn't asked us not to track it, add it to the * internal tracking list. */ if ((flags & ALLOC_DONT_TRACK) == 0) { dvmAddTrackedAlloc((Object*)ptr, NULL); } } else { /* * The allocation failed; throw an OutOfMemoryError. */ throwOOME(); } return ptr; } |
這個函式定義在檔案dalvik/vm/alloc/Heap.cpp中。
在Java堆分配記憶體前後,要對Java堆進行加鎖和解鎖,避免多個執行緒同時對Java堆進行操作。這分別是通過函式dvmLockHeap和dvmunlockHeap來實現的。真正執行記憶體分配的操作是通過呼叫另外一個函式tryMalloc來完成的。如果分配成功,則記錄當前執行緒成功分配的記憶體位元組數和物件數等資訊。否則的話,就記錄當前執行緒失敗分配的記憶體位元組數和物件等資訊。有了這些資訊之後,我們就可以通過DDMS等工具來對應用程式的記憶體使用資訊進行統計了。
最後,如果分配記憶體成功,並且引數flags的ALLOC_DONT_TRACK位設定為0,那麼需要將新建立的物件增加到Dalvik虛擬機器內部的一個引用表去。儲存在這個內部引用表的物件在執行GC時,會新增到根集去,以便可以正確地判斷物件的存活。
另一方面,如果分配記憶體失敗,那麼就是時候呼叫函式throwOOME丟擲一個OOM異常了。
我們接下來繼續分析函式tryMalloc的實現,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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; } |
這個函式定義在檔案dalvik/vm/alloc/Heap.cpp中。
函式tryMalloc的執行流程就如圖2所示:
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進行記憶體分配。這是最後一次努力了,成功與事都到此為止。
這裡涉及到的關鍵函式有三個,分別是dvmHeapSourceAlloc、dvmHeapSourceAllocAndGrow和gcForMalloc。後面一個我們在接下來一篇文章分析Dalvik虛擬機器的垃圾收集過程時再分析。現在重點分析前面兩個函式。
函式dvmHeapSourceAlloc的實現如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
void* dvmHeapSourceAlloc(size_t n) { HS_BOILERPLATE(); 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); if (ptr == NULL) { return NULL; } uintptr_t zero_begin = (uintptr_t)ptr; uintptr_t zero_end = (uintptr_t)ptr + n; ...... uintptr_t begin = ALIGN_UP_TO_PAGE_SIZE(zero_begin); uintptr_t end = zero_end & ~(uintptr_t)(SYSTEM_PAGE_SIZE - 1); ...... if (begin < end) { ...... madvise((void*)begin, end - begin, MADV_DONTNEED); ...... memset((void*)end, 0, zero_end - end); ...... zero_end = begin; } memset((void*)zero_begin, 0, zero_end - zero_begin); } else { ptr = mspace_calloc(heap->msp, 1, n); if (ptr == NULL) { return NULL; } } countAllocation(heap, ptr); ...... if (gDvm.gcHeap->gcRunning || !hs->hasGcThread) { ...... return ptr; } if (heap->bytesAllocated > heap->concurrentStartBytes) { ...... dvmSignalCond(&gHs->gcThreadCond); } return ptr; } |
這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。
從前面Dalvik虛擬機器Java堆建立過程分析一文可知,gHs是一個全域性變數,它指向一個HeapSource結構。在這個HeapSource結構中,有一個heaps陣列,其中第一個元素描述的是Active堆,第二個元素描述的是Zygote堆。
通過巨集hs2heap可以獲得HeapSource結構中的Active堆,儲存在本地變數heap中。巨集hs2heap的實現如下所示:
1 |
#define hs2heap(hs_) (&((hs_)->heaps[0])) |
這個巨集定義在檔案dalvik/vm/alloc/HeapSource.cpp中。
在前面Dalvik虛擬機器Java堆建立過程分析一文中,我們解釋了Java堆有起始大小、最大值、增長上限值、最小空閒值、最大空閒值和目標利用率等引數。在Dalvik虛擬機器內部,還有一個稱為軟限制(Soft Limit)的引數。堆軟限制是一個與堆目標利率相關的引數。
Java堆的Soft Limit開始的時候設定為最大允許的整數值。但是每一次GC之後,Dalvik虛擬機器會根據Active堆已經分配的記憶體位元組數、設定的堆目標利用率和Zygote堆的大小,重新計算Soft Limit,以及別外一個稱為理想大小(Ideal Size)的值。如果此時只有一個堆,即只有Active堆沒有Zygote堆,那麼Soft Limit就等於Ideal Size。如果此時有兩個堆,那麼Ideal Size就等於Zygote堆的大小再加上Soft Limit值,其中Soft Limit值就是此時Active堆的大小,它是根據Active堆已經分配的記憶體位元組數和設定的堆目標利用率計算得到的。
這個Soft Limit值到底有什麼用呢?它主要是用來限制Active堆無節制地增長到最大值的,而是要根據預先設定的堆目標利用率來控制Active有節奏地增長到最大值。這樣可以更有效地使用堆記憶體。想象一下,如果我們一開始Active堆的大小設定為最大值,那麼就很有可能造成已分配的記憶體分佈在一個很大的範圍。這樣隨著Dalvik虛擬機器不斷地執行,Active堆的記憶體碎片就會越來越來重。相反,如果我們施加一個Soft Limit,那可以儘量地控制已分配的記憶體都位於較緊湊的範圍內。這樣就可以有效地減少碎片。
回到函式dvmHeapSourceAlloc中,引數n描述的是要分配的記憶體大小,而heap->bytesAllocated描述的是Active堆已經的記憶體大小。由於函式dvmHeapSourceAlloc是不允許增長Active堆的大小的,因此當(heap->bytesAllocated + n)的值大於Active堆的Soft Limit時,就直接返回一個NULL值表示分配記憶體失敗。
如果要分配的記憶體不會超過Active堆的Soft Limit,那麼就要考慮Dalivk虛擬機器在啟動時是否指定了低記憶體模式。我們可以通過-XX:LowMemoryMode選項來讓Dalvik虛擬機器執行低記憶體模式下。在低記憶體模式和非低記憶體模組中,物件記憶體的分配方式有所不同。
在低記憶體模式中,Dalvik虛擬機器假設物件不會馬上就使用分配到的記憶體,因此,它就通過系統介面madvice和MADV_DONTNEED標誌告訴核心,剛剛分配出去的記憶體在近期內不會使用,核心可以該記憶體對應的物理頁回收。當分配出去的記憶體被使用時,核心就會重新給它對映物理頁,這樣就可以做按需分配實體記憶體,適合在記憶體小的裝置上執行。這裡有三點需要注意。
第一點是Dalvik虛擬機器要求分配給物件的記憶體初始化為0,但是在低記憶體模式中,是使用函式mspace_malloc來分配記憶體,該函式不會將分配的記憶體初始化為0,因此我們需要自己去初始化這塊記憶體。
第二點是對於被系統介面madvice標記為MADV_DONTNEED的記憶體,是不需要我們將它初始化為0的,一來是因為這是無用功(對應的物理而可能會被核心回收),二來是因為當這些記憶體在真正使用時,核心在為它們對映物理頁的同時,也會同時對映的物理頁初始為0。
第三點是在呼叫系統介面madvice時,指定的記憶體地址以及記憶體大小都必須以頁大小為邊界的,但是函式mspace_malloc分配出來的記憶體的地址只能保證對齊到8個位元組,因此,我們是有可能不能將所有分配出來的記憶體都通過系統介面madvice標記為MADV_DONTNEED的。這時候對於不能標記為MADV_DONTNEED的記憶體,就需要呼叫memset來將它們初始化為0。
在非低記憶體模式中,處理的邏輯就簡單很多了,直接使用函式mspace_calloc在Active堆上分配指定的記憶體大小即可,同時該函式還會將分配的記憶體初始化為0,正好是可以滿足Dalvik虛擬機器的要求。
注意,由於記憶體碎片的存在,即使是要分配的記憶體沒有超出Active堆的Soft Limit,在呼叫函式mspace_malloc和函式mspace_calloc的時候,仍然有可能出現無法成功分配記憶體的情況。在這種情況下,都直接返回一個NULL值給呼叫者。
在分配成功的情況下,函式dvmHeapSourceAlloc還需要做兩件事情。
第一件事情是呼叫函式countAllocation來計賬,它的實現如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 |
static void countAllocation(Heap *heap, const void *ptr) { assert(heap->bytesAllocated < mspace_footprint(heap->msp)); heap->bytesAllocated += mspace_usable_size(ptr) + HEAP_SOURCE_CHUNK_OVERHEAD; heap->objectsAllocated++; HeapSource* hs = gDvm.gcHeap->heapSource; dvmHeapBitmapSetObjectBit(&hs->liveBits, ptr); assert(heap->bytesAllocated < mspace_footprint(heap->msp)); } |
這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。
函式countAllocation要計的賬有三個:
1. 記錄Active堆當前已經分配的位元組數。
2. 記錄Active堆當前已經分配的物件數。
3. 呼叫函式dvmHeapBitmapSetObjectBit將新分配的物件在Live Heap Bitmap上對應的位設定為1,也就是說將新建立的物件標記為是存活的。關於Live Heap Bitmap,可以參考前面Dalvik虛擬機器Java堆建立過程分析一文。
回到函式dvmHeapSourceAlloc中,它需要做的第二件事情是檢查當前Active堆已經分配的位元組數是否已經大於預先設定的Concurrent GC閥值heap->concurrentStartBytes。如果大於的話,那麼就需要通知GC執行緒執行一次Concurrent GC。當然,如果當前GC執行緒已經在進行垃圾回收,那麼就不用通知了。當gDvm.gcHeap->gcRunning的值等於true時,就表示GC執行緒正在進行垃圾回收。
這樣,函式dvmHeapSourceAlloc的實現就分析完成了,接下來我們繼續分析另外一個函式dvmHeapSourceAllocAndGrow的實現,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
void* dvmHeapSourceAllocAndGrow(size_t n) { ...... HeapSource *hs = gHs; Heap* heap = hs2heap(hs); void* ptr = dvmHeapSourceAlloc(n); if (ptr != NULL) { return ptr; } size_t oldIdealSize = hs->idealSize; if (isSoftLimited(hs)) { ...... hs->softLimit = SIZE_MAX; ptr = dvmHeapSourceAlloc(n); if (ptr != NULL) { ...... snapIdealFootprint(); return ptr; } } ptr = heapAllocAndGrow(hs, heap, n); if (ptr != NULL) { ...... snapIdealFootprint(); } else { ...... setIdealFootprint(oldIdealSize); } return ptr; } |
這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。
函式dvmHeapSourceAllocAndGrow首先是在不增加Active堆的前提下,呼叫我們前面分析的函式dvmHeapSourceAlloc來分配大小為n的記憶體。如果分配成功,那麼就可以直接返回了。否則的話,繼續往前處理。
在繼續往前處理之前,先記錄一下當前Zygote堆和Active堆的大小之和oldIdealSize。這是因為後面我們可能會修改Active堆的大小。當修改了Active堆的大小,但是仍然不能成功分配大小為n的記憶體,那麼就需要恢復之前Zygote堆和Active堆的大小。
如果Active堆設定有Soft Limit,那麼函式isSoftLimited的返回值等於true。在這種情況下,先將Soft Limit去掉,再呼叫函式dvmHeapSourceAlloc來分配大小為n的記憶體。如果分配成功,那麼在將分配得到的地址返回給呼叫者之前,需要呼叫函式snapIdealFootprint來修改Active堆的大小。也就是說,在去掉Active堆的Soft Limit之後,可以成功地分配到大小為n的記憶體,這時候就需要相應的增加Soft Limit的大小。
如果Active堆沒有設定Soft Limit,或者去掉Soft Limit之後,仍然不能成功地在Active堆上分配在大小為n的記憶體,那麼這時候就得出大招了,它會呼叫函式heapAllocAndGrow將Java堆的大小設定為允許的最大值,然後再在Active堆上分配大小為n的記憶體。
最後,如果能成功分配到大小為n的記憶體,那麼就呼叫函式snapIdealFootprint來重新設定Active堆的當前大小。否則的話,就呼叫函式setIdealFootprint來恢復之前Active堆的大小。這是因為雖然分配失敗,但是前面仍然做了修改Active堆大小的操作。
為了更好地理解函式dvmHeapSourceAllocAndGrow的實現,我們繼續分析一下涉及到的函式isSoftLimited、setIdealFootprint、snapIdealFootprint和heapAllocAndGrow的實現。
函式isSoftLimited的實現如下所示:
1 2 3 4 5 6 7 8 9 10 |
static bool isSoftLimited(const HeapSource *hs) { /* softLimit will be either SIZE_MAX or the limit for the * active mspace. idealSize can be greater than softLimit * if there is more than one heap. If there is only one * heap, a non-SIZE_MAX softLimit should always be the same * as idealSize. */ return hs->softLimit <= hs->idealSize; } |
這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。
根據我們前面的分析,hs->softLimit描述的是Active堆的大小,而hs->idealSize描述的是Zygote堆和Active堆的大小之和。
當只有一個堆時,即只有Active堆時,如果設定了Soft Limit,那麼它的大小總是等於Active堆的大小,即這時候hs->softLimit總是等於hs->idealSize。如果沒有設定Soft Limit,那麼它的值會被設定為SIZE_MAX值,這會就會保證hs->softLimit大於hs->idealSize。也就是說,當只有一個堆時,函式isSoftLimited能正確的反映Active堆是否設定有Soft Limit。
當有兩個堆時,即Zygote堆和Active堆同時存在,那麼如果設定有Soft Limit,那麼它的值就總是等於Active堆的大小。由於hs->idealSize描述的是Zygote堆和Active堆的大小之和,因此就一定可以保證hs->softLimit小於等於hs->idealSize。如果沒有設定Soft Limit,即hs->softLimit的值等於SIZE_MAX,那麼就一定可以保證hs->softLimit的值大於hs->idealSize的值。也就是說,當有兩個堆時,函式isSoftLimited也能正確的反映Active堆是否設定有Soft Limit。
函式setIdealFootprint的實現如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
static void setIdealFootprint(size_t max) { HS_BOILERPLATE(); HeapSource *hs = gHs; size_t maximumSize = getMaximumSize(hs); if (max > maximumSize) { LOGI_HEAP("Clamp target GC heap from %zd.%03zdMB to %u.%03uMB", FRACTIONAL_MB(max), FRACTIONAL_MB(maximumSize)); max = maximumSize; } /* Convert max into a size that applies to the active heap. * Old heaps will count against the ideal size. */ size_t overhead = getSoftFootprint(false); size_t activeMax; if (overhead < max) { activeMax = max - overhead; } else { activeMax = 0; } setSoftLimit(hs, activeMax); hs->idealSize = max; } |
這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。
函式setIdealFootprint的作用是要將Zygote堆和Active堆的大小之和設定為max。在設定之前,先檢查max值是否大於Java堆允許的最大值maximum。如果大於的話,那麼就屬於將max的值修改為maximum。
接下為是以引數false來呼叫函式getSoftFootprint來獲得Zygote堆的大小overhead。如果max的值大於Zygote堆的大小overhead,那麼從max中減去overhead,就可以得到Active堆的大小activeMax。如果max的值小於等於Zygote堆的大小overhead,那麼就說明要將Active堆的大小activeMax設定為0。
最後,函式setIdealFootprint呼叫函式setSoftLimit設定Active堆的當前大小,並且將Zygote堆和Active堆的大小之和記錄在hs->idealSize中。
這裡又涉及到兩個函式getSoftFootprint和setSoftLimit,我們同樣對它們進行分析。
函式getSoftFootprint的實現如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 |
static size_t getSoftFootprint(bool includeActive) { HS_BOILERPLATE(); HeapSource *hs = gHs; size_t ret = oldHeapOverhead(hs, false); if (includeActive) { ret += hs->heaps[0].bytesAllocated; } return ret; } |
這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。
函式getSoftFootprint首先呼叫函式oldHeapOverhead獲得Zygote堆的大小ret。當引數includeActive等於true時,就表示要返回的是Zygote堆的大小再加上Active堆當前已經分配的記憶體位元組數的值。而當引數includeActive等於false時,要返回的僅僅是Zygote堆的大小。
函式oldHeapOverhead的實現如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static size_t oldHeapOverhead(const HeapSource *hs, bool includeActive) { size_t footprint = 0; size_t i; if (includeActive) { i = 0; } else { i = 1; } for (/* i = i */; i < hs->numHeaps; i++) { //TODO: include size of bitmaps? If so, don't use bitsLen, listen to .max footprint += mspace_footprint(hs->heaps[i].msp); } return footprint; } |
這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。
從這裡就可以看出,當引數includeActive等於true時,函式oldHeapOverhead返回的是Zygote堆和Active堆的大小之和,而當引數includeActive等於false時,函式oldHeapOverhead僅僅返回Zygote堆的大小。注意,hs->heaps[0]指向的是Active堆,而hs->heaps[1]指向的是Zygote堆。這一點可以參考前面Dalvik虛擬機器Java堆建立過程分析一文。
回到函式setIdealFootprint中,我們繼續分析函式setSoftLimit的實現,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static void setSoftLimit(HeapSource *hs, size_t softLimit) { ...... mspace msp = hs->heaps[0].msp; size_t currentHeapSize = mspace_footprint(msp); if (softLimit < currentHeapSize) { ...... mspace_set_footprint_limit(msp, currentHeapSize); hs->softLimit = softLimit; } else { ...... mspace_set_footprint_limit(msp, softLimit); hs->softLimit = SIZE_MAX; } } |
這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。
函式setSoftLimit首先是獲得Active堆的當前大小currentHeapSize。如果引數softLimit的值小於Active堆的當前大小currentHeapSize,那麼就意味著要給Active堆設定一個Soft Limit,這時候主要就是將引數softLimit的儲存在hs->softLimit中。另一方面,如果引數softLimit的值大於等於Active堆的當前大小currentHeapSize,那麼就意味著要去掉Active堆的Soft Limit,並且將Active堆的大小設定為引數softLimit的值。
回到函式dvmHeapSourceAlloc中,我們繼續分析最後兩個函式snapIdealFootprint和heapAllocAndGrow的實現,
函式snapIdealFootprint的實同如下所示:
1 2 3 4 5 6 |
static void snapIdealFootprint() { HS_BOILERPLATE(); setIdealFootprint(getSoftFootprint(true)); } |
這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。
函式snapIdealFootprint通過呼叫前面分析的函式getSoftFootprint和setIdealFootprint來調整Active堆的大小以及Soft Limit值。回憶一下函式dvmHeapSourceAlloc呼叫snapIdealFootprint的情景,分別是在修改了Active堆的Soft Limit或者將Active堆的大小設定為允許的最大值,並且成功在Active堆分配了指定大小的記憶體之後進行的。這樣就需要呼叫函式snapIdealFootprint將Active堆的大小設定為實際使用的大小(而不是允許的最大值),以及重新設定Soft Limit值。
函式heapAllocAndGrow的實現如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static void* heapAllocAndGrow(HeapSource *hs, Heap *heap, size_t n) { ...... size_t max = heap->maximumSize; mspace_set_footprint_limit(heap->msp, max); void* ptr = dvmHeapSourceAlloc(n); ...... mspace_set_footprint_limit(heap->msp, mspace_footprint(heap->msp)); return ptr; } |
這個函式定義在檔案dalvik/vm/alloc/HeapSource.cpp中。
函式heapAllocAndGrow使用最激進的辦法來在引數heap描述的堆上分配記憶體。在我們這個情景中,引數heap描述的就是Active堆上。它首先將Active堆的大小設定為允許的最大值,接著再呼叫函式dvmHeapSourceAlloc在上面分配大小為n的記憶體。接著再通過函式mspace_footprint獲得分配了n個位元組之後Active堆的大小,並且將該值設定為Active堆的當前大小限制。這就相當於是將Active堆的當前大小限制值從允許設定的最大值減少為一個剛剛合適的值。
至此,我們就分析完成了Dalvik虛擬機器為新建立的物件分配記憶體的過程。只有充分理解了物件記憶體的分配過程之後,我們才能夠更好地理解物件記憶體的釋放過程,也就是Dalvik虛擬機器的垃圾收集過程。在接下來的一篇文章中,我們就將詳細分析Dalvik虛擬機器的垃圾收集過程,敬請關注!更多的資訊也可以關注老羅的新浪微博:http://weibo.com/shengyangluo。