JVM、DVM(Dalvik VM)和ART虛擬機對比

本文在於幫助大家快速的有一定深度的瞭解Android虛擬機。如果讀者期望更加深入的瞭解相關的內容,可以根據文末給出的參考資料繼續往下學習。如果覺得文中內容有什麼錯誤,歡迎讀者朋友指正,同時如需要轉載請註明出處http://blog.csdn.net/evan_man/article/details/52414390,謝謝!
Android系統使用Dalvik Virtual Machine (DVM)作爲其虛擬機,所有安卓程序都運行在安卓系統進程裏,每個進程對應着一個Dalvik虛擬機實例。他們都提供了對象生命週期管理、堆棧管理、線程管理、安全和異常管理以及垃圾回收等重要功能,各自擁有一套完整的指令系統。Android之所以不直接使用JVM作爲其虛擬機的原因有很多,版權問題我們暫且擱置一邊,本文將首先在技術上對DVM和JVM進行比較,然後重點對Dalvik虛擬機的垃圾回收機制進行介紹,文章末尾再對Android5.0之後使用的新型虛擬機——ART虛擬機進行簡單介紹。

DVM vs JVM

共同點:

  • 都是解釋執行
  • 都是每個 OS 進程運行一個 VM,並運行一個單獨的程序
  • 在較新版本中(Froyo / Sun JDK 1.5)都實現了相當程度的 JIT compiler(即時編譯) 用於提速。
    • JIT(Just In Time,即時編譯技術)對於熱代碼(使用頻率高的字節碼)直接轉換成彙編代碼;

不同點:

  • dvm執行的是.dex格式文件,jvm執行的是.class文件。class文件和dex之間可以相互轉換具體流程如下圖,多個class文件轉變成一個dex文件會引發一些問題,具體如下:
    • 方法數受限:多個class文件變成一個dex文件所帶來的問題就是方法數超過65535時報錯,由此引出MultiDex技術,具體資料同學可以google下。
    • class文件去冗餘:class文件存在很多的冗餘信息,dex工具會去除冗餘信息(多個class中的字符串常量合併爲一個,比如對於Ljava/lang/Oject字符常量,每個class文件基本都有該字符常量,存在很大的冗餘),並把所有的.class文件整合到.dex文件中。減少了I/O操作,提高了類的查找速度。
  • 許多GC實現都是在對象開頭的地方留一小塊空間給GC標記用。Dalvik VM則不同,在進行GC的時候會單獨申請一塊空間,以位圖的形式來保存整個堆上的對象的標記,在GC結束後就釋放該空間。 (關於這一點後面的Dalvik垃圾回收機制還會更加深入的介紹)
  • dvm是基於寄存器的虛擬機 而jvm執行是基於虛擬棧的虛擬機。這類的不同是最要命的,因爲它將導致一系列的問題,具體如下:
    • dvm速度快!寄存器存取速度比棧快的多,dvm可以根據硬件實現最大的優化,比較適合移動設備。JAVA虛擬機基於棧結構,程序在運行時虛擬機需要頻繁的從棧上讀取寫入數據,這個過程需要更多的指令分派與內存訪問次數,會耗費很多CPU時間。
    • 指令數小!dvm基於寄存器,所以它的指令是二地址和三地址混合,指令中指明瞭操作數的地址;jvm基於棧,它的指令是零地址,指令的操作數對象默認是操作數棧中的幾個位置。這樣帶來的結果就是dvm的指令數相對於jvm的指令數會小很多,jvm需要多條指令而dvm可能只需要一條指令。
    • jvm基於棧帶來的好處是可以做的足夠簡單,真正的跨平臺,保證在低硬件條件下能夠正常運行。而dvm操作平臺一般指明是ARM系統,所以採取的策略有所不同。需要注意的是dvm基於寄存器,但是這也是個映射關係,如果硬件沒有足夠的寄存器,dvm將多出來的寄存器映射到內存中。

Dalvik虛擬機

談到垃圾回收自然而然的想到了堆,Dalvik的堆結構相對於JVM的堆結構有所區別,這主要體現在Dalvik將堆分成了Active堆和Zygote堆,這裏大家只要知道Zygote堆是Zygote進程在啓動的時候預加載的類、資源和對象(具體gygote進程預加載了哪些類,詳見文末的附錄),除此之外的所有對象都是存儲在Active堆中的。對於爲何要將堆分成gygote和Active堆,這主要是因爲Android通過fork方法創建到一個新的gygote進程,爲了儘可能的避免父進程和子進程之間的數據拷貝,fork方法使用寫時拷貝技術,寫時拷貝技術簡單講就是fork的時候不立即拷貝父進程的數據到子進程中,而是在子進程或者父進程對內存進行寫操作時是纔對內存內容進行復制,Dalvik的gygote堆存放的預加載的類都是Android核心類和java運行時庫,這部分內容很少被修改,大多數情況父進程和子進程共享這塊內存區域。通常垃圾回收重點對Active堆進行回收操作,Dalvik爲了對堆進行更好的管理創建了一個Card Table、兩個Heap Bitmap和一個Mark Stack數據結構。

Dalvik創建對象流程

當Dalvik虛擬機的解釋器遇到一個new指令時,它就會調用函數Object* dvmAllocObject(ClassObject* clazz, int flags)。期間完成的動作有( 注意:Java堆分配內存前後,要對Java堆進行加鎖和解鎖,避免多個線程同時對Java堆進行操作。下面所說的堆指的是Active堆):
  1. 調用函數dvmHeapSourceAlloc在Java堆上分配指定大小的內存,成功則返回,否則下一步。
  2. 執行一次GC, GC執行完畢後,再次調用函數dvmHeapSourceAlloc在Java堆上分配指定大小的內存,成功則返回,否則下一步。
  3. 首先將堆的當前大小設置爲Dalvik虛擬機啓動時指定的Java堆最大值,然後進行內存分配,成功返回失敗下一步。這裏調用的函數是 dvmHeapSourceAllocAndGrow
  4. 調用函數gcForMalloc來執行GC,這裏的GC和第二步的GC,區別在於這裏回收軟引用對象引用的對象,如果還是失敗拋出OOM異常。這裏調用的函數是dvmHeapSourceAllocAndGrow

Dalvik回收對象流程

Dalvik的垃圾回收策略默認是標記擦除回收算法,即Mark和Sweep兩個階段。標記與清理的回收算法一個明顯的區別就是會產生大量的垃圾碎片,因此程序中應該避免有大量不連續小碎片的時候分配大對象,同時爲了解決碎片問題,Dalvik虛擬機通過使用dlmalloc技術解決,關於後者讀者另行google。下面我們對Mark階段進行簡單介紹。
Mark階段使用了兩個Bitmap來描述堆的對象,一個稱爲Live Bitmap,另一個稱爲Mark Bitmap。Live Bitmap用來標記上一次GC時被引用的對象,也就是沒有被回收的對象,而Mark Bitmap用來標記當前GC有被引用的對象。當Live Bitmap被標記爲1,但是在Mark Bitmap中標記爲0的對象表明該對象需要被回收。此外在Mark階段往往要求其它線程處於停止狀態,因此Mark又分爲並行和串行兩種方式,並行的Mark分爲兩個階段:1)、只標記gc_root對象,即在GC開始的瞬間被全局變量、棧變量、寄存器等所引用的對象,該階段不允許垃圾回收線程之外的線程處於運行狀態。2)、有條件的並行運行其它線程,使用Card Table記錄在垃圾收集過程中對象的引用情況。整個Mark 階段都是通過Mark Stack來實現遞歸檢查被引用的對象,即在當前GC中存活的對象。標記過程類似用一個棧把第一階段得到的gc_root放入棧底,然後依次遍歷它們所引用的對象(通過出棧入棧),即用棧數據結構實現了對每個gc_root的遞歸。
Dalvik的GC類型共有四種:
  • GC_CONCURRENT: 表示是在已分配內存達到一定量之後觸發的GC。
  • GC_FOR_MALLOC: 表示是在堆上分配對象時內存不足觸發的GC。
  • GC_BEFORE_OOM: 表示是在準備拋OOM異常之前進行的最後努力而觸發的GC。
  • GC_EXPLICIT: 表示是應用程序調用System.gc、VMRuntime.gc接口或者收到SIGUSR1信號時觸發的GC。
其中GC_FOR_MALLOC、GC_CONCURRENT和GC_BEFORE_OOM三種類型的GC都是在分配對象的過程觸發的。垃圾回收具體都是通過調用函數void dvmCollectGarbageInternal(const GcSpec* spec) 來執行垃圾回收,該函數的參數GcSpec結構體定義見本文的附錄。對於函數dvmCollectGarbageInternal的內部邏輯,即垃圾回收流程,根據垃圾回收線程和工作線程的關係分爲並行GC和非並行GC。前者在回收階段有選擇性的停止當前工作線程,後者在垃圾回收階段停止所有工作線程。但是並行GC需要多執行一次標記根集對象以及遞歸標記那些在GC過程被訪問了的對象的操作,意味着並行GC需要花費更多的CPU資源。dvmCollectGarbageInternal函數的內部邏輯如下:(本文末尾的附錄中給出了一個對應的流程圖)
  1. 調用函數dvmSuspendAllThreads掛起所有的線程,以免它們干擾GC。
    1. 這裏如何掛起其它線程呢?其實就是每個線程在運行過程中會週期性的檢測自身的一個標誌位,通過這個標誌位我們可以告知線程停止運行。
  2. 調用函數dvmHeapBeginMarkStep初始化Mark Stack,並且設定好GC範圍。
    1. Mark Stack其實是一個object指針數組
  3. 調用函數dvmHeapMarkRootSet標記根集對象。
    1. Mark的第一階段,主要分爲兩大類:1)Dalvik虛擬機內部使用的全局對象(維護在一個hash表中);2)應用程序正在使用的對象(維護在一個調用棧中)
  4. 調用函數dvmClearCardTable清理Card Table。(只在並行gc發生)
    1. Card Table記錄記錄在Zygote堆上分配的對象在垃圾收集執行過程中對在Active堆上分配的對象的引用。
  5. 調用函數dvmUnlock解鎖堆。這個是針對調用函數dvmCollectGarbageInternal執行GC前的堆鎖定操作。(只在並行gc發生)
  6. 調用函數dvmResumeAllThreads喚醒第1步掛起的線程。(只在並行gc發生)
    1. 此時非gc線程可以開始工作,這部分線程對堆的操作記錄在CardTable上面,gc則進行Mark的第二階段
  7. 調用函數dvmHeapScanMarkedObjects從第3步獲得的根集對象開始,歸遞標記所有被根集對象引用的對象。
  8. 調用函數dvmLockHeap重新鎖定堆。這個是針對前面第5步的操作。(只在並行gc發生)
  9. 調用函數dvmSuspendAllThreads重新掛起所有的線程。這個是針對前面第6步的操作。(只在並行gc發生)
    1. 這裏需要再次停止工作線程,用來解決前面線程對堆的少部分的操作,這個過程很快。
  10. 調用函數dvmHeapReMarkRootSet更新根集對象。因爲有可能在第4步到第6步的執行過程中,有線程創建了新的根集對象。(只在並行gc發生)
  11. 調用函數dvmHeapReScanMarkedObjects歸遞標記那些在第4步到第6步的執行過程中被修改的對象。這些對象記錄在Card Table中。(只在並行gc發生)
  12. 調用函數dvmHeapProcessReferences處理那些被軟引用(Soft Reference)、弱引用(Weak Reference)和影子引用(Phantom Reference)引用的對象,以及重寫了finalize方法的對象。這些對象都是需要特殊處理的。
  13. 調用函數dvmHeapSweepSystemWeaks回收系統內部使用的那些被弱引用引用的對象。
  14. 調用函數dvmHeapSourceSwapBitmaps交換Live Bitmap和Mark Bitmap。
    1. 執行了前面的13步之後,所有還被引用的對象在Mark Bitmap中的bit都被設置爲1。
    2. Live Bitmap記錄的是當前GC前還被引用着的對象。
    3. 通過交換這兩個Bitmap,就可以使得當前GC完成之後,使得Live Bitmap記錄的是下次GC前還被引用着的對象。
  15. 調用函數dvmUnlock解鎖堆。這個是針對前面第8步的操作。(只在並行gc發生)
  16. 調用函數dvmResumeAllThreads喚醒第9步掛起的線程。(只在並行gc發生)
  17. 調用函數dvmHeapSweepUnmarkedObjects回收那些沒有被引用的對象。沒有被引用的對象就是那些在執行第14步之前,在Live Bitmap中的bit設置爲1,但是在Mark Bitmap中的bit設置爲0的對象。
  18. 調用函數dvmHeapFinishMarkStep重置Mark Bitmap以及Mark Stack。這個是針對前面第2步的操作。
  19. 調用函數dvmLockHeap重新鎖定堆。這個是針對前面第15步的操作。(只在並行gc發生)
  20. 調用函數dvmHeapSourceGrowForUtilization根據設置的堆目標利用率調整堆的大小。
  21. 調用函數dvmBroadcastCond喚醒那些等待GC執行完成再在堆上分配對象的線程。(只在並行gc發生)
  22. 調用函數dvmResumeAllThreads喚醒第1步掛起的線程。(只在非並行gc發生)
  23. 調用函數dvmEnqueueClearedReferences將那些目標對象已經被回收了的引用對象增加到相應的Java隊列中去,以便應用程序可以知道哪些引用引用的對象已經被回收了。
總結:
通過上面的流程分析,我們知道了並行和串行gc的區別在於:
  1. 並行gc會在mark第二階段將非gc線程喚醒;當mark的第二階段完成之後,再次停止非gc線程;利用cardtable的信息再次進行一個mark操作,此時的mark操作比第一個mark操作要快得多。
  2. 並行gc會在sweep階段將非gc線程喚醒。
  3. 串行gc會在垃圾回收開始就暫停所有非gc線程,知道垃圾回收結束。
  4. 並行gc涉及到兩次的mark操作,消耗cpu時間。

ART虛擬機

在Android5.0中,ART取代了Dalvik虛擬機(安卓在4.4中發佈了ART)。ART虛擬機直接執行本地機器碼;而Dalvik虛擬機運行的是DEX字節碼需要通過解釋器執行。安卓運行時從Dalvik虛擬機替換成ART虛擬機,並不要求開發者重新將自己的應用直接編譯成目標機器碼,應用程序仍然是一個包含dex字節碼的apk文件,這主要得益於AOT技術,AOT(Ahead Of Time)是相對JIT(Just In Time)而言的;也就是在APK運行之前,就對其包含的Dex字節碼進行翻譯,得到對應的本地機器指令,於是就可以在運行時直接執行了。ART應用安裝的時候把dex中的字節碼將被編譯成本地機器碼,之後每次打開應用,執行的都是本地機器碼。去除了運行時的解釋執行,效率更高,啓動更快。
ART運行時內部使用的Java堆的主要組成包括Image Space、Zygote Space、Allocation Space和Large Object Space四個Space,兩個Mod Union Table,一個Card Table,兩個Heap Bitmap,兩個Object Map(Live 和 Mark Object Map),以及三個Object Stack (Live、Mark、Allocation Stack)。具體結構圖參考附錄。
Image Space和Zygote Space之間,隔着一段用來映射system@[email protected]@classes.oat文件的內存。system@[email protected]@classes.oat是一個OAT文件,它是由在系統啓動類路徑中的所有DEX文件翻譯得到的,Image Space映射的是一個system@[email protected]@classes.dex文件,這個文件保存的是在生成system@[email protected]@classes.oat這個OAT文件的時候需要預加載的類對象,這些需要預加載的類由/system/framework/framework.jar文件裏面的preloaded-classes文件指定。以後只要系統啓動類路徑中的DEX文件不發生變化(即不發生更新升級),那麼以後每次系統啓動只需要將文件system@[email protected]@classes.dex直接映射到內存即可。
由於system@[email protected]@classes.dex文件保存的是一些預先創建的對象,並且這些對象之間可能會互相引用,因此我們必須保證system@[email protected]@classes.dex文件每次加載到內存的地址都是固定的。這個固定的地址保存在system@[email protected]@classes.dex文件開頭的一個Image Header中。此外,system@[email protected]@classes.dex文件也依賴於system@[email protected]@classes.oat文件,因此也會將後者固定加載到Image Space的末尾。
Image Space是不能分配新對象的。Image Space和Zygote Space在Zygote進程和應用程序進程之間進行共享,而Allocation Space是每個進程都獨立地擁有一份。

ART的運行原理:

  1. 在Android系統啓動過程中創建的Zygote進程利用ART運行時導出的Java虛擬機接口創建ART虛擬機。
  2. APK在安裝的時候,打包在裏面的classes.dex文件會被工具dex2oat翻譯成本地機器指令,最終得到一個ELF格式的oat文件。
  3. APK運行時,上述生成的oat文件會被加載到內存中,並且ART虛擬機可以通過裏面的oatdata和oatexec段找到任意一個類的方法對應的本地機器指令來執行。
    1. oat文件中的oatdata包含用來生成本地機器指令的dex文件內容
    2. oat文件中的oatexec包含有生成的本地機器指令。
注意:
這裏將DEX文件中的類和方法稱之爲DEX類和DEX方法,將OTA中的類和方法稱之爲OTA類和OTA方法,ART運行時將類和方法稱之爲Class和ArtMethod。
ART中一個已經加載的Class對象包含了一系列的ArtField對象和ArtMethod對象,其中,ArtField對象用來描述成員變量信息,而ArtMethod用來描述成員函數信息。對於每一個ArtMethod對象,它都有一個解釋器入口點和一個本地機器指令入口點。

ART找到一個類和方法的流程:

  1. 在DEX文件中找到目標DEX類的編號,並且以這個編號爲索引,在OAT文件中找到對應的OAT類。
  2. 在DEX文件中找到目標DEX方法的編號,並且以這個編號爲索引,在上一步找到的OAT類中找到對應的OAT方法。
  3. 使用上一步找到的OAT方法的成員變量begin_和code_offset_,計算出該方法對應的本地機器指令。
上面的流程對應給出了流程圖,具體內容參考附錄。

ART運行時對象的創建過程:

可以分配內存的Space有三個:Zygote Space、Allocation Space和Large Object Space。不過,Zygote Space在還沒有劃分出Allocation Space之前,就在Zygote Space上分配,而當Zygote Space劃分出Allocation Space之後,就只能在Allocation Space上分配。因此實際上應用運行的時候能夠分配內存也就Allocation 和 Large Object Space兩個。
而分配的對象究竟是存入上面的哪個Space呢?滿足如下三個條件的內存,存入Large Object Space:1)Zygote Space已經劃分除了Allocation Space,2)分配對象是原子類型數組,如int[] byte[] boolean[], 3)分配的內存大小大於一定的門限值。
對於分配對象時內存不足的問題,是通過垃圾回收和在允許範圍內增長堆大小解決的。由於垃圾回收會影響程序,因此ART運行時採用力度從小到大的進垃圾回收策略。一旦力度小的垃圾回收執行過後能滿足分配要求,那就不需要進行力度大的垃圾回收了。這跟dalvik虛擬機的對象分配策略也是類似的。

ART垃圾回收流程

並行GC流程圖如下:
  1. 調用子類實現的成員函數InitializePhase執行GC初始化階段。
  2. 獲取用於訪問Java堆的鎖。
  3. 調用子類實現的成員函數MarkingPhase執行GC並行標記階段。
  4. 釋放用於訪問Java堆的鎖。
  5. 掛起所有的ART運行時線程。
  6. 調用子類實現的成員函數HandleDirtyObjectsPhase處理在GC並行標記階段被修改的對象。
  7. 恢復第4步掛起的ART運行時線程。
  8. 重複第5到第7步,直到所有在GC並行階段被修改的對象都處理完成。
  9. 獲取用於訪問Java堆的鎖。
  10. 調用子類實現的成員函數ReclaimPhase執行GC回收階段。
  11. 釋放用於訪問Java堆的鎖。
  12. 調用子類實現的成員函數FinishPhase執行GC結束階段
非並行GC流程圖如下:
  1. 調用子類實現的成員函數InitializePhase執行GC初始化階段。
  2. 掛起所有的ART運行時線程。
  3. 調用子類實現的成員函數MarkingPhase執行GC標記階段。
  4. 調用子類實現的成員函數ReclaimPhase執行GC回收階段。
  5. 恢復第2步掛起的ART運行時線程。
  6. 調用子類實現的成員函數FinishPhase執行GC結束階段
通過兩者的對比可以得出如下結論(與Dalvik大同小異):
  • 非並行GC在垃圾回收的整個過程中暫停了所有非gc線程
  • 並行GC在一開始只是對堆進行加鎖,對於那些暫時並不會在堆中分配的內存的線程不起作用,它們依然可以運行,但是會造成對象的引用發生變化,但是這段時間的引用發生的變化被記錄了下來。之後系統會停止所有線程,對上面記錄的數據進行處理,然後喚起所有線程,系統進入垃圾回收階段。

附錄:

Gygote堆預加載的類有:

該文件所指明的類就是通常gygote進程在創建時預加載的類,基本上囊括Android開發中大部分使用到的類,如View、Activity以及java運行時庫等都會進行預加載。

Dalvik對應的GC類型結構體定義如下:

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回收階段調用的dvmCollectGarbageInternal()函數所得到的流程圖


圖.1、dvmCollectGarbageInternal函數針對並行和串行兩種gc的流程圖

下圖是ART的堆結構圖


圖.2、ART的堆結構
Mod Union Table對象
  • 一個用來記錄在GC並行階段在Image Space上分配的對象對在Zygote Space和Allocation Space上分配的對象的引用。
  • 另一個用來記錄在GC並行階段在Zygote Space上分配的對象對在Allocation Space上分配的對象的引用。
Allocation Stack:用來記錄上一次GC後分配的對象,用來實現類型爲Sticky的Mark Sweep Collector。
Live Stack:配合allocation_stack_一起使用,用來實現類型爲Sticky的Mark Sweep Collector。
Mark Stack:用來在GC過程中實現遞歸對象標記

ART找到一個類和方法的流程:


圖.3、在OAT文件中查找類方法的本地機器指令的過程
       我們從左往右來看圖.3。首先是根據類簽名信息從包含在OAT文件裏面的DEX文件中查找目標Class的編號,然後再根據這個編號找到在OAT文件中找到對應的OatClass。接下來再根據方法簽名從包含在OAT文件裏面的DEX文件中查找目標方法的編號,然後再根據這個編號在前面找到的OatClass中找到對應的OatMethod。有了這個OatMethod之後,我們就根據它的成員變量begin_和code_offset_找到目標類方法的本地機器指令了。其中,從DEX文件中根據簽名找到類和方法的編號要求對DEX文件進行解析,這就需要利用Dalvik虛擬機的知識了。
參考資料:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章