JVM、DVM(Dalvik VM)和ART虛擬機的區別


                                   DVM vs JVM
1、共同點:

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

2、不同點:

(1)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操作,提高了類的查找速度。

(2)許多GC實現都是在對象開頭的地方留一小塊空間給GC標記用。Dalvik VM則不同,在進行GC的時候會單獨申請一塊空間,以位圖的形式來保存整個堆上的對象的標記,在GC結束后就釋放該空間。 (關於這一點后面的Dalvik垃圾回收機制還會更加深入的介紹)
(3)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數據結構。

1、Dalvik創建對象流程

  當Dalvik虛擬機的解釋器遇到一個new指令時,它就會調用函數Object* dvmAllocObject(ClassObject* clazz, int flags)。期間完成的動作有( 注意:Java堆分配內存前后,要對Java堆進行加鎖和解鎖,避免多個線程同時對Java堆進行操作。下面所說的堆指的是Active堆):

  • 調用函數dvmHeapSourceAlloc在Java堆上分配指定大小的內存,成功則返回,否則下一步。
  • 執行一次GC, GC執行完畢后,再次調用函數dvmHeapSourceAlloc在Java堆上分配指定大小的內存,成功則返回,否則下一步。
  • 首先將堆的當前大小設置為Dalvik虛擬機啟動時指定的Java堆最大值,然后進行內存分配,成功返回失敗下一步。這里調用的函數是 dvmHeapSourceAllocAndGrow
  • 調用函數gcForMalloc來執行GC,這里的GC和第二步的GC,區別在於這里回收軟引用對象引用的對象,如果還是失敗拋出OOM異常。這里調用的函數是dvmHeapSourceAllocAndGrow

2、Dalvik回收對象流程

  Dalvik的垃圾回收策略默認是標記擦除回收算法,即MarkSweep兩個階段。標記與清理的回收算法一個明顯的區別就是會產生大量的垃圾碎片,因此程序中應該避免有大量不連續小碎片的時候分配大對象,同時為了解決碎片問題,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。
    這里如何掛起其它線程呢?其實就是每個線程在運行過程中會周期性的檢測自身的一個標志位,通過這個標志位我們可以告知線程停止運行。
(2)調用函數dvmHeapBeginMarkStep初始化Mark Stack,並且設定好GC范圍。
    Mark Stack其實是一個object指針數組
(3)調用函數dvmHeapMarkRootSet標記根集對象。
Mark的第一階段,主要分為兩大類:

  • Dalvik虛擬機內部使用的全局對象(維護在一個hash表中);
  • 應用程序正在使用的對象(維護在一個調用棧中)

(4)調用函數dvmClearCardTable清理Card Table。(只在並行gc發生)
    Card Table記錄記錄在Zygote堆上分配的對象在垃圾收集執行過程中對在Active堆上分配的對象的引用。
(5)調用函數dvmUnlock解鎖堆。這個是針對調用函數dvmCollectGarbageInternal執行GC前的堆鎖定操作。(只在並行gc發生)
(6)調用函數dvmResumeAllThreads喚醒第1步掛起的線程。(只在並行gc發生)
    此時非gc線程可以開始工作,這部分線程對堆的操作記錄在CardTable上面,gc則進行Mark的第二階段
(7) 調用函數dvmHeapScanMarkedObjects從第3步獲得的根集對象開始,歸遞標記所有被根集對象引用的對象。
(8)調用函數dvmLockHeap重新鎖定堆。這個是針對前面第5步的操作。(只在並行gc發生)
(9)調用函數dvmSuspendAllThreads重新掛起所有的線程。這個是針對前面第6步的操作。(只在並行gc發生)
這里需要再次停止工作線程,用來解決前面線程對堆的少部分的操作,這個過程很快。
(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。

  • 執行了前面的13步之后,所有還被引用的對象在Mark Bitmap中的bit都被設置為1。
  • Live Bitmap記錄的是當前GC前還被引用着的對象。
  • 通過交換這兩個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的區別在於:

  • 並行gc會在mark第二階段將非gc線程喚醒;當mark的第二階段完成之后,再次停止非gc線程;利用cardtable的信息再次進行一個mark操作,此時的mark操作比第一個mark操作要快得多。
  • 並行gc會在sweep階段將非gc線程喚醒。
  • 串行gc會在垃圾回收開始就暫停所有非gc線程,知道垃圾回收結束。
  • 並行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 SpaceZygote SpaceAllocation SpaceLarge 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@framework@boot.art@classes.oat文件的內存。system@framework@boot.art@classes.oat是一個OAT文件,它是由在系統啟動類路徑中的所有DEX文件翻譯得到的,Image Space映射的是一個system@framework@boot.art@classes.dex文件,這個文件保存的是在生成system@framework@boot.art@classes.oat這個OAT文件的時候需要預加載的類對象,這些需要預加載的類由/system/framework/framework.jar文件里面的preloaded-classes文件指定。以后只要系統啟動類路徑中的DEX文件不發生變化(即不發生更新升級),那么以后每次系統啟動只需要將文件system@framework@boot.art@classes.dex直接映射到內存即可。
  由於system@framework@boot.art@classes.dex文件保存的是一些預先創建的對象,並且這些對象之間可能會互相引用,因此我們必須保證system@framework@boot.art@classes.dex文件每次加載到內存的地址都是固定的。這個固定的地址保存在system@framework@boot.art@classes.dex文件開頭的一個Image Header中。此外,system@framework@boot.art@classes.dex文件也依賴於system@framework@boot.art@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段找到任意一個類的方法對應的本地機器指令來執行。

  • oat文件中的oatdata包含用來生成本地機器指令的dex文件內容
  • oat文件中的oatexec包含有生成的本地機器指令。

注意:
  這里將DEX文件中的類和方法稱之為DEX類和DEX方法,將OTA中的類和方法稱之為OTA類和OTA方法,ART運行時將類和方法稱之為Class和ArtMethod。
  ART中一個已經加載的Class對象包含了一系列的ArtField對象和ArtMethod對象,其中,ArtField對象用來描述成員變量信息,而ArtMethod用來描述成員函數信息。對於每一個ArtMethod對象,它都有一個解釋器入口點和一個本地機器指令入口點。

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

  在DEX文件中找到目標DEX類的編號,並且以這個編號為索引,在OAT文件中找到對應的OAT類。
  在DEX文件中找到目標DEX方法的編號,並且以這個編號為索引,在上一步找到的OAT類中找到對應的OAT方法。
  使用上一步找到的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垃圾回收流程:

1、並行GC流程圖如下:

  • 調用子類實現的成員函數InitializePhase執行GC初始化階段。
  • 獲取用於訪問Java堆的鎖。
  • 調用子類實現的成員函數MarkingPhase執行GC並行標記階段。
  • 釋放用於訪問Java堆的鎖。
  • 掛起所有的ART運行時線程。
  • 調用子類實現的成員函數HandleDirtyObjectsPhase處理在GC並行標記階段被修改的對象。
  • 恢復第4步掛起的ART運行時線程。
  • 重復第5到第7步,直到所有在GC並行階段被修改的對象都處理完成。
  • 獲取用於訪問Java堆的鎖。
  • 調用子類實現的成員函數ReclaimPhase執行GC回收階段。
  • 釋放用於訪問Java堆的鎖。
  • 調用子類實現的成員函數FinishPhase執行GC結束階段

2、非並行GC流程圖如下:

調用子類實現的成員函數InitializePhase執行GC初始化階段。掛起所有的ART運行時線程。

  • 調用子類實現的成員函數MarkingPhase執行GC標記階段。
  • 調用子類實現的成員函數ReclaimPhase執行GC回收階段。
  • 恢復第2步掛起的ART運行時線程。
  • 調用子類實現的成員函數FinishPhase執行GC結束階段

通過兩者的對比可以得出如下結論(與Dalvik大同小異):

  • 非並行GC在垃圾回收的整個過程中暫停了所有非gc線程
  • 並行GC在一開始只是對堆進行加鎖,對於那些暫時並不會在堆中分配的內存的線程不起作用,它們依然可以運行,但是會造成對象的引用發生變化,但是這段時間的引用發生的變化被記錄了下來。之后系統會停止所有線程,對上面記錄的數據進行處理,然后喚起所有線程,系統進入垃圾回收階段。

附錄:

Gygote堆預加載的類有:


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對象

(1)一個用來記錄在GC並行階段在Image Space上分配的對象對在Zygote Space和Allocation Space上分配的對象的引用。

(2)另一個用來記錄在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虛擬機的知識了。


————————————————
版權聲明:本文為CSDN博主「evan_man」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/evan_man/article/details/52414390


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM