ART執行時為新建立物件分配記憶體的過程分析

發表於2015-07-22

從前面ART執行時Java堆建立過程分析一文可以知道,在ART執行時中,主要用來分配物件的堆空間Zygote Space和Allocation Space的底層使用的都是匿名共享記憶體,並且通過C庫提供的malloc和free介面來分進行管理。這樣就可以通過dlmalloc技術來儘量解決碎片問題。這一點與我們在前面Dalvik虛擬機器為新建立物件分配記憶體的過程分析一文提到的Dalvik虛擬機器解決堆記憶體碎片問題的方法是一樣的。因此,接下來在分析ART執行時為新建立物件分配的過程中,主要會分析它是如何解決記憶體不足的問題的。

ART執行時為新建立物件分配的過程如圖1所示:

圖1 ART執行時為新建立物件分配記憶體的過程

        對比Dalvik虛擬機器為新建立物件分配記憶體的過程分析一文的圖2,可以發現,ART執行時和Dalvik虛擬機器為新建立物件分配記憶體的過程幾乎是一模一樣的,它們的區別僅僅是在於垃圾收集的方式和策略不同。

從前面Android執行時ART執行類方法的過程分析一文可以知道,ART執行時為從DEX位元組碼翻譯得到的Native程式碼提供的一個函式呼叫表中,有一個pAllocObject介面,是用來分配物件的。當ART執行時以Quick模式執行在ARM體系結構時,上述提到的pAllocObject介面由函式art_quick_alloc_object來實現。因此,接下來我們就從函式art_quick_alloc_object的實現開始分析ART執行時為新建立物件分配記憶體的過程。

函式art_quick_alloc_object的實現如下所示:

這個函式定義在檔案art/runtime/arch/arm/quick_entrypoints_arm.S中。

這是一段ARM彙編,我們需要注意的一點是Native程式碼呼叫ART執行時提供的物件分配介面的引數傳遞方式。其中,引數type_idx描述的是要分配的物件的型別,通過暫存器r0傳遞,引數method描述的是當前呼叫的類方法,通過暫存器r1傳遞。

函式art_quick_alloc_object是通過呼叫另外一個函式artAllocObjectFromCode來分配物件的。函式art_quick_alloc_object除了傳遞前面描述的引數type_idx和method給函式artAllocObjectFromCode之外,還會傳遞另外的兩個引數。其中一個是描述當前執行緒的一個Thread物件,該物件總是儲存在暫存器r9中,現在由於要通過引數的形式傳遞給另外一個函式,因此就將它放在暫存器r2。另外一個是棧指標sp,也是由於要通過引數的形式的傳遞另外一個函式,這裡也會將它放在暫存器r3中。

函式artAllocObjectFromCode的實現如下所示:

這個函式定義在檔案art/runtime/entrypoints/quick/quick_alloc_entrypoints.cc中。

函式artAllocObjectFromCode又是通過呼叫另外一個函式AllocObjectFromCode來分配物件的。不過,在呼叫函式AllocObjectFromCode之前,函式artAllocObjectFromCode會先呼叫另外一個函式FinishCalleeSaveFrameSetup在當前呼叫棧幀中儲存一個執行時資訊。這個執行時資訊描述的是接下來要呼叫的方法的型別為Runtime::kRefsOnly,也就是由被呼叫者儲存那些不是用來傳遞引數的通用暫存器,即除了r0-r3的其它通用暫存器。

函式AllocObjectFromCode的實現如下所示:

這個函式定義在檔案art/runtime/entrypoints/entrypoint_utils.h中。

引數type_idx描述的是要分配的物件的型別,函式AllocObjectFromCode需要將它解析為一個Class物件,以便可以獲得更多的資訊進行記憶體分配。

函式AllocObjectFromCode首先是在當前呼叫類方法method的Dex Cache中檢查是否已經存在一個與引數type_idx對應的Class物件。如果已經存在,那麼就說明引數type_idx描述的物件型別已經被載入和解析過了,因此這時候就可以直接拿來使用。否則的話,就通過呼叫儲存在當前執行時物件內部的一個ClassLinker物件的成員函式ResolveType來對引數type_idx描述的物件型別進行載入和解析。關於Dex Cache的知識,可以引數前面Android執行時ART執行類方法的過程分析一文,而物件型別(即類)的載入和解析過程可以參考前面Android執行時ART載入類和方法的過程分析一文。

得到了要分配的物件的型別klass之後,如果引數access_check的值等於true,那麼就對該型別進行檢查,即檢查它是否可以例項化以及是否可以訪問。如果檢查通過,或者不需要檢查,那麼接下來還要確保型別klass是已經初始化過了的。前面的檢查都沒有問題之後,最後函式AllocObjectFromCode就呼叫Class類的成員函式AllocObject來分配一個型別為klass的物件。

Class類的成員函式AllocObject的實現如下所示:

這個函式定義在檔案art/runtime/mirror/class.cc中。

這裡我們就終於看到呼叫ART執行時內部的Heap物件的成員函式AllocObject在堆上分配物件了,其中,要分配的大小儲存在當前Class物件的成員變數object_size_中。

Heap類的成員函式AllocObject的實現如下所示:

這個函式定義在檔案art/runtime/gc/heap.cc中。

Heap類的成員函式AllocObject首先是要確定要在哪個Space上分配記憶體。可以分配記憶體的Space有三個,分別Zygote Space、Allocation Space和Large Object Space。不過,Zygote Space在還沒有劃分出Allocation Space之前,就在Zygote Space上分配,而當Zygote Space劃分出Allocation Space之後,就只能在Allocation Space上分配。從前面ART執行時Java堆建立過程分析一文可以知道,Heap類的成員變數alloc_space_在Zygote Space在還沒有劃分出Allocation Space之前指向Zygote Space,劃分之後就指向Allocation Space。Large Object Space則始終由Heap類的成員變數large_object_space_指向。

只要滿足以下三個條件,就在Large Object Space上分配,否則就在Zygote Space或者Allocation Space上分配:

1. 請求分配的記憶體大於等於Heap類的成員變數large_object_threshold_指定的值。這個值等於3 * kPageSize,即3個頁面的大小。

2. 已經從Zygote Space劃分出Allocation Space,即Heap類的成員變數have_zygote_space_的值等於true。

3. 被分配的物件是一個原子型別陣列,即byte陣列、int陣列和boolean陣列等。

確定好要在哪個Space上分配記憶體之後,就可以呼叫Heap類的成員函式Allocate進行分配了。如果分配成功,Heap類的成員函式Allocate就返回新分配的物件,儲存在變數obj中。接下來再做三件事情:

1. 呼叫Object類的成員函式SetClass設定新分配物件obj的型別。

2. 呼叫Heap類的成員函式RecordAllocation記錄當前的記憶體分配狀況。

3. 檢查當前已經分配出去的記憶體是否已經達到由Heap類的成員變數concurrent_start_bytes_設定的閥值。如果達到,那麼就呼叫Heap類的成員函式RequestConcurrentGC通知GC執行一次並行GC。關於執行並行GC的閥值,接下來分要ART執行時的垃圾收集過程中再詳細分析。

另一方面,如果Heap類的成員函式Allocate分配記憶體失敗,則Heap類的成員函式AllocObject丟擲一個OOM異常。

接下來,我們先分析Heap類的成員函式RecordAllocation的實現,接著再分析Heap類的成員函式Allocate的實現。因為後者的執行流程比較複雜,而前者的執行流程比較簡單。我們先分析容易的,以免打斷後面的分析。

Heap類的成員函式RecordAllocation的實現如下所示:

這個函式定義在檔案art/runtime/gc/heap.cc中。

Heap類的成員函式RecordAllocation首先是記錄當前已經分配的記憶體位元組數以及物件數,接著再將新分配的對角壓入到Heap類的成員變數allocation_stack_描述的Allocation Stack中去。後面這一點與Dalvik虛擬機器的做法是不一樣的。Dalvik虛擬機器直接將新分配出來的物件記錄在Live Bitmap中,具體可以參考前面Dalvik虛擬機器為新建立物件分配記憶體的過程分析一文。ART執行時之所以要將新分配的物件壓入到Allocation Stack中去,是為了以後可以執行Sticky GC。

注意,如果不能成功將將新分配的對角壓入到Allocation Stack中,就說明上次GC以來,新分配的物件太多了,因此這時候就需要執行一個Sticky GC,將Allocation Stack裡面的垃圾進行回收,然後再嘗試將新分配的物件壓入到Allocation Stack中,直到成功為止。

接下來我們就重點分析Heap類的成員函式Allocate的實現,以便可以瞭解新建立物件在堆上分配的具體過程,如下所示:

這個函式定義在檔案art/runtime/gc/heap.cc中。

Heap類的成員函式Allocate首先呼叫成員函式TryToAllocate嘗試在不執行GC的情況下進行記憶體分配。如果分配失敗,再呼叫成員函式AllocateInternalWithGc進行帶GC的記憶體分配。

Heap類的成員函式Allocate是一個模板函式,不同型別的Space會導致呼叫不同過載的成員函式TryToAllocate進行不帶GC的記憶體分配。雖然可以用來分配記憶體的Space有Zygote Space、Allocation Space和Large Object Space三個,但是前兩者的型別是相同的,因此實際上只有兩個不同過載版本的成員函式TryToAllocate,它們的實現如下所示:

這兩個函式定義在檔案art/runtime/gc/heap.cc中。

Heap類兩個過載版本的成員函式TryToAllocate的實現邏輯都幾乎是相同的,首先是呼叫另外一個成員函式IsOutOfMemoryOnAllocation判斷分配請求的記憶體後是否會超過堆的大小限制。如果超過,則分配失敗;否則的話再在指定的Space進行記憶體分配。

Heap類的成員函式IsOutOfMemoryOnAllocation的實現如下所示:

這個函式定義在檔案art/runtime/gc/heap.cc中。

Heap類的成員變數num_bytes_allocated_描述的是目前已經分配出去的記憶體位元組數,成員變數max_allowed_footprint_描述的是目前堆可分配的最大記憶體位元組數,成員變數growth_limit_描述的是目前堆允許增長到的最大記憶體位元組數。這裡需要注意的一點是,max_allowed_footprint_是Heap類施加的一個限制,不會對各個Space實際可分配的最大記憶體位元組數產生影響,並且各個Space在建立的時候,已經把自己可分配的最大記憶體數設定為允許使用的最大記憶體位元組數的。

如果目前堆已經分配出去的記憶體位元組數再加上請求分配的記憶體位元組數new_footprint小於等於目前堆可分配的最大記憶體位元組數max_allowed_footprint_,那麼分配出請求的記憶體位元組數之後不會造成OOM,因此Heap類的成員函式IsOutOfMemoryOnAllocation就返回false。。

另一方面,如果目前堆已經分配出去的記憶體位元組數再加上請求分配的記憶體位元組數new_footprint大於目前堆可分配的最大記憶體位元組數max_allowed_footprint_,並且也大於目前堆允許增長到的最大記憶體位元組數growth_limit_,那麼分配出請求的記憶體位元組數之後造成OOM,因此Heap類的成員函式IsOutOfMemoryOnAllocation就返回true。

剩下另外一種情況,目前堆已經分配出去的記憶體位元組數再加上請求分配的記憶體位元組數new_footprint大於目前堆可分配的最大記憶體位元組數max_allowed_footprint_,但是小於等於目前堆允許增長到的最大記憶體位元組數growth_limit_,這時候就要看情況了會不會出現OOM了。如果ART執行時執行在非並行GC的模式中,即Heap類的成員變數concurrent_gc_等於false,那麼取決於允不允許增長堆的大小,即引數grow的值。如果不允許,那麼Heap類的成員函式IsOutOfMemoryOnAllocation就返回true,表示當前請求的分配會造成OOM。如果允許,那麼Heap類的成員函式IsOutOfMemoryOnAllocation就會修改目前堆可分配的最大記憶體位元組數max_allowed_footprint_,並且返回false,表示允許當前請求的分配。這意味著,在非並行GC執行模式中,在分配記憶體過程中遇到記憶體不足,並且當前可分配記憶體還未達到增長上限時,要等到執行完成一次非並行GC後,才能成功分配到記憶體,因為每次執行完成GC之後,都會按照預先設定的堆目標利用率來增長堆的大小。

另一方面,如果ART執行時執行在並行GC的模式中,那麼只要前堆已經分配出去的記憶體位元組數再加上請求分配的記憶體位元組數new_footprint不地超過目前堆允許增長到的最大記憶體位元組數growth_limit_,那麼就不管允不允許增長堆的大小,都認為不會發生OOM,因此Heap類的成員函式IsOutOfMemoryOnAllocation就返回false。這意味著,在並行GC執行模式中,在分配記憶體過程中遇到記憶體不足,並且當前可分配記憶體還未達到增長上限時,無非等到執行並行GC後,就有可能成功分配到記憶體,因為實際執行記憶體分配的Space可分配的最大記憶體位元組數是足夠的。

回到前面Heap類的成員函式TryToAllocate中,從前面ART執行時Java堆建立過程分析一文可以知道,對於Large Object Space版本的成員函式TryToAllocate,呼叫的是LargeObjectMapSpace類的成員函式Alloc進行記憶體分配,而對於Zygote Space或者Allocation Space版本的成員函式TryToAllocate,如果成員變數running_on_valgrind_的值等於true,就呼叫ValgrindDlMallocSpace類的成員函式AllocNonvirtual進行記憶體分配,否則就呼叫DlMallocSpace類的成員函式Alloc進行記憶體分配。我們假設Heap類的成員變數running_on_valgrind_的值等於false,因此接下來我們主要分析LargeObjectMapSpace類的成員函式Alloc和DlMallocSpace類的成員函式Alloc的實現。

LargeObjectMapSpace類的成員函式Alloc的實現如下所示:

這個函式定義在檔案art/runtime/gc/space/large_object_space.cc中。

從這裡就可以看到,Large Object Map Space分配記憶體的邏輯是很簡單的,直接就是呼叫MemMap類的靜態成員函式MapAnonymous建立一塊指定大小的匿名記憶體,然後再將該匿名共享記憶體新增到成員變數large_objects_描述的一個向量中去,最後更新內部的各個統計資料。

DlMallocSpace類的成員函式Alloc的實現如下所示:

這個函式定義在檔案art/runtime/gc/space/dlmalloc_space.cc中。

DlMallocSpace類的成員函式Alloc呼叫另外一個成員函式AllocNonvirtual來進行記憶體分配,後者的實現如下所示:

這個函式定義在檔案art/runtime/gc/space/dlmalloc_space-inl.h中。

DlMallocSpace類的成員函式AllocNonvirtual首先是呼叫另外一個成員函式AllocWithoutGrowthLocked在不增長Space的大小的前提下進行記憶體分配,分配成功之後再呼叫函式memset對分配出來的記憶體進行清空,最後將分配出來的記憶體返回給呼叫者。

DlMallocSpace類的成員函式AllocWithoutGrowthLocked的實現如下所示:

這個函式定義在檔案art/runtime/gc/space/dlmalloc_space-inl.h中。

從前面ART執行時Java堆建立過程分析一文可以知道,DlMallocSpace底層使用的匿名共享記憶體塊被封裝成一個mspace物件,並且儲存在成員變數mspace_中,因此這裡就可以直接呼叫C庫提供的mspace_malloc介面進行記憶體分配。使用mspace_malloc分配的記憶體會自動被清空,因此這裡不用再手動清空。DlMallocSpace類的成員函式AllocWithoutGrowthLocked在將分配出來的記憶體返回給呼叫者之前,同樣是會更新內部的各個統計資料。

回到前面Heap類的成員函式Allocate中,在呼叫成員函式TryToAllocate不能成功分配指定大小的記憶體塊之後,接下來就繼續呼叫成員函式AllocateInternalWithGc執行帶GC的記憶體分配,它的實現如下所示:

這個函式定義在檔案art/runtime/gc/heap.cc中。

Heap類的成員函式AllocateInternalWithGc主要是通過垃圾回收來滿足請求分配的記憶體,它的執行邏輯如下所示:

1. 呼叫Heap類的成員函式WaitForConcurrentGcToComplete檢查是否有並行GC正在執行。如果有的話,就等待其執行完成,並且得到它的型別last_gc。如果last_gc如果不等於collector::kGcTypeNone,就表示有並行GC並且已經執行完成,因此就可以呼叫Heap類的成員函式TryToAllocate在不增長當前堆大小的前提下再次嘗試分配請求的記憶體了。如果分配成功,則返回得到的記憶體起始地址給呼叫者。否則的話,繼續往下執行。

2.  依次執行kGcTypeSticky、kGcTypePartial和kGcTypeFull三種型別的GC。每次GC執行完畢,都嘗試呼叫Heap類的成員函式TryToAllocate在不增長當前堆大小的前提下再次嘗試分配請求的記憶體。如果分配記憶體成功,則返回得到的記憶體起始地址給呼叫者,並且不再執行下一個種型別的GC。

這裡需要注意的一點是,kGcTypeSticky、kGcTypePartial和kGcTypeFull三種型別的GC的垃圾回收力度是依次加強:kGcTypeSticky只回收上次GC後在Allocation Space中新分配的垃圾物件;kGcTypePartial只回收Allocation Space的垃圾物件;kGcTypeFull同時回收Zygote Space和Allocation Space的垃圾物件。通過這種策略,就有可能以最小代價解決分配物件時遇到的內在不足問題。不過,對於型別為kGcTypeSticky和kGcTypePartial的GC,它們的執行是前提的

型別為kGcTypeSticky的GC的執行程式碼雖然是最小的,但是它能夠回收的垃圾也是最小的。如果回收的垃圾不足於滿足請求分配的記憶體,那就相當於做了一次無用功了。因此,執行型別為kGcTypeSticky的GC需要滿足兩個條件。第一個條件是上次GC後在Allocation Space上分配的記憶體要達到一定的閥值,這樣才有比較大的概率回收到較多的記憶體。第二個條件Allocation Space剩餘的未分配記憶體要達到一定的閥值,這樣可以保證在回收得到較少記憶體時,也有比較大的概率滿足請求分配的記憶體。前一個閥值定義在Heap類的成員變數min_alloc_space_size_for_sticky_gc_中,它的值設定為2M,而上次GC以來分配的記憶體通過當前Allocation Space的大小估算得到,即通過呼叫Heap類的成員變數alloc_space_指向的一個DlMallocSpace物件的成員函式Size獲得。後一個閥值定義在Heap類的成員變數min_remaining_space_for_sticky_gc_中,它的值設定為1M,而Allocation Space剩餘的未分配記憶體可以用Allocation Space的總大小減去當前Allocation Space的大小得到。通過呼叫Heap類的成員變數alloc_space_指向的一個DlMallocSpace物件的成員函式Capacity獲得其總大小。

型別為kGcTypePartial的GC的執行前提是已經從Zygote Space中劃分出Allocation Space。從前面ART執行時Java堆建立過程分析一文可以知道,當Heap類的成員變數have_zygote_space_的值等於true時,就表明已經從Zygote Space中劃分出Allocation Space了。因此,在這種情況下,就可以執行型別為kGcTypePartial的GC了。

每一種型別的GC都是通過呼叫Heap類的成員函式CollectGarbageInternal來執行。注意這時候呼叫Heap類的成員函式CollectGarbageInternal傳遞的第三個引數為false,表示不對那些只被軟引用物件引用的物件進行回收。如果上述的三種型別的GC執行完畢,還是不能滿足分配請求的記憶體,則繼續往下執行。

3. 經過前面三種型別的GC後還是不能成功分配到記憶體,那就說明能夠回收的記憶體還是太小了,因此,這時候只能通過在允許範圍內增長堆的大小來滿足記憶體分配請求了。前面分析Heap類的成員函式TryToAlloctate時,將第四個引數設定為true,即可在允許範圍內增長堆大小的前提下進行記憶體分配。如果在允許範圍內增長了堆的大小還是不能成功分配到請求的記憶體,那就只能出最後的一個大招了。

4.  最後的大招是首先執行一個型別為kGcTypeFull的、要求回收那些只被軟引用物件引用的物件的GC,接著再在允許範圍內增長堆大小的前提下嘗試分配記憶體。這一次如果還是失敗,那就真的是記憶體不足了。

至此,我們就對ART執行時在堆上為新建立物件分配記憶體的過程分析完成了。從中我們就可以看到,ART執行時面臨的最大挑戰就是記憶體不足問題,它要通過在允許範圍內增長堆大小以及垃圾回收兩個手段來解決。其中,垃圾回收會對程式造成影響,因此在執行垃圾回收時,使用的力度要從小到大。在接下來的一篇文章中,我們就將詳細分析這些力度不同的垃圾回收是如何實現的,敬請關注!更多的資訊也可以關注老羅的新浪微博:http://weibo.com/shengyangluo

相關文章