Dalvik虛擬機器為新建立物件分配記憶體的過程分析

發表於2015-07-18

記憶體碎片問題其實是一個通用的問題,不單止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,如下所示:

這個程式碼段定義在檔案dalvik/vm/mterp/out/InterpC-portable.cpp中。

關於Dalvik虛擬機器的直譯器的實現,可以參考Dalvik虛擬機器的執行過程分析一文,上面這段程式碼首先是找到要建立的物件的型別clazz,接著以其作為引數呼叫函式dvmAllocObject為要建立的物件分配記憶體。另外一個引數ALLOC_DONT_TRACK是告訴Dalvik虛擬機器的堆管理器,要分配的物件是一個根集物件,不需要對它進行跟蹤。因為根集物件在GC時是會自動被追蹤處理的。

函式dvmAllocObject的實現如下所示:

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

函式dvmAllocObject呼叫函式dvmMalloc從Java堆中分配一塊指定大小的記憶體給新建立的物件使用。如果分配成功,那麼接下來就先使用巨集DVM_OBJECT_INIT來初始化新建立對物件的成員變數clazz,使得新建立的物件可以與某個特定的類關聯起來,接著再呼叫函式dvmTrackAllocation記錄當前的記憶體分配資訊,以便通知DDMS。

函式dvmMalloc返回的只是一塊記憶體地址,這是沒有型別的。但是由於每一個Java物件都是從Object類繼承下來的,因此,函式dvmAllocObject可以將獲得的沒有型別的記憶體塊強制轉換為一個Object物件。

Object類的定義如下所示:

這個類定義在檔案dalvik/vm/oo/Object.h中。

Object類有兩個成員變數:clazz和lock。其中,成員變數clazz的型別為ClassObject,它對應於Java層的java.lang.Class類,用來描述物件所屬的類。成員變數lock是一個鎖,正是因為有了這個成員變數,在Java層中,每一個物件都可以當鎖使用。

理解了Object類的定義之後,我們繼續分析函式dvmMalloc的實現,如下所示:

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

在Java堆分配記憶體前後,要對Java堆進行加鎖和解鎖,避免多個執行緒同時對Java堆進行操作。這分別是通過函式dvmLockHeap和dvmunlockHeap來實現的。真正執行記憶體分配的操作是通過呼叫另外一個函式tryMalloc來完成的。如果分配成功,則記錄當前執行緒成功分配的記憶體位元組數和物件數等資訊。否則的話,就記錄當前執行緒失敗分配的記憶體位元組數和物件等資訊。有了這些資訊之後,我們就可以通過DDMS等工具來對應用程式的記憶體使用資訊進行統計了。

最後,如果分配記憶體成功,並且引數flags的ALLOC_DONT_TRACK位設定為0,那麼需要將新建立的物件增加到Dalvik虛擬機器內部的一個引用表去。儲存在這個內部引用表的物件在執行GC時,會新增到根集去,以便可以正確地判斷物件的存活。

另一方面,如果分配記憶體失敗,那麼就是時候呼叫函式throwOOME丟擲一個OOM異常了。

我們接下來繼續分析函式tryMalloc的實現,如下所示:

這個函式定義在檔案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的實現如下所示:

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

從前面Dalvik虛擬機器Java堆建立過程分析一文可知,gHs是一個全域性變數,它指向一個HeapSource結構。在這個HeapSource結構中,有一個heaps陣列,其中第一個元素描述的是Active堆,第二個元素描述的是Zygote堆。

通過巨集hs2heap可以獲得HeapSource結構中的Active堆,儲存在本地變數heap中。巨集hs2heap的實現如下所示:

這個巨集定義在檔案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來計賬,它的實現如下所示:

這個函式定義在檔案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的實現,如下所示:

這個函式定義在檔案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的實現如下所示:

這個函式定義在檔案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的實現如下所示:

這個函式定義在檔案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的實現如下所示:

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

函式getSoftFootprint首先呼叫函式oldHeapOverhead獲得Zygote堆的大小ret。當引數includeActive等於true時,就表示要返回的是Zygote堆的大小再加上Active堆當前已經分配的記憶體位元組數的值。而當引數includeActive等於false時,要返回的僅僅是Zygote堆的大小。

函式oldHeapOverhead的實現如下所示:

這個函式定義在檔案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的實現,如下所示:

這個函式定義在檔案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的實同如下所示:

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

函式snapIdealFootprint通過呼叫前面分析的函式getSoftFootprint和setIdealFootprint來調整Active堆的大小以及Soft Limit值。回憶一下函式dvmHeapSourceAlloc呼叫snapIdealFootprint的情景,分別是在修改了Active堆的Soft Limit或者將Active堆的大小設定為允許的最大值,並且成功在Active堆分配了指定大小的記憶體之後進行的。這樣就需要呼叫函式snapIdealFootprint將Active堆的大小設定為實際使用的大小(而不是允許的最大值),以及重新設定Soft Limit值。

函式heapAllocAndGrow的實現如下所示:

這個函式定義在檔案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

相關文章