Dalvik 和 ART

Java 虛擬機在前面的系列文章中有所介紹(http://www.lxweimin.com/c/84fea797b420),Android 所使用的虛擬機(Dalvik 和 ART)與 JVM 有所不同,這篇文章來介紹一下。

1. Dalvik 虛擬機

Dalvik 虛擬機(Dalvik Virtual Machine),簡稱 Dalvik VM 或者 DVM。它是 Google 專門為 Android 平臺開發的虛擬機,運行在 Android 運行時庫中。DVM 并不是一個 Java 虛擬機,原因在下面介紹。

1.1 DVM 與 JVM 的區別

DVM 之所以不是一個 JVM,主要原因是 DVM 并沒有遵循 JVM 規范來實現,與 JVM 主要區別如下:

1. 基于的架構不同

JVM 的執行的指令是基于棧結構,這就意味著需要去棧中讀寫數據,所需的指令會很多,會導致速度變慢,對于性能有限的移動設備,顯然不合適。DVM 是基于寄存器的,沒有基于棧的虛擬機在復制數據時使用的大量的出入棧指令,同時指令更緊湊、更簡介。但是由于指定了操作數,所以指令會比基于棧的指令大,但是由于指令數量的減少,總的代碼不會增加多少。

2. 執行的字節碼不同

在 Java SE 程序中,Java 類被編譯成一個或多個 .class 文件,并打包成 jar 文件,而后 JVM 會通過相應的 .class 文件和 jar 文件獲取對相應的字節碼。執行順序為:.java 文件 → .class 文件 → .jar 文件,而 DVM 會用 dx 工具將所有的 .class 文件轉換為一個 .dex 文件,然后 DVM 會從該 .dex 文件讀取指令和數據。執行順序為:.java 文件 → .class 文件 → .dex 文件。
jar 文件和 apk 文件結構如圖:


文件結構對比圖

關于 class 文件結構的詳細說明,可參考:

http://www.lxweimin.com/p/c23355cb712d

當 JVM 加載 .jar 文件的時候,會加載里面所有的 .class 文件,這種加載方式對于性能有限的移動設備不合適。在 .apk 文件中,一般情況下只包含一個 .dex 文件,這個 .dex 文件把所有的 .class 文件信息整合在一起,這樣就提升了加載速度。.class 文件種也會存在一些冗余信息,dex 工具會去除冗余信息,并把所有的 .class 文件整合到 .dex 文件種,減少 I/O 操作,加快了類的查找速度。

3. DVM 允許在有限的內存中同時運行多個進程

DVM 經過優化,允許在有限的內存中同時運行多個進程。在 Android 中的每一個應用都運行在一個 DVM 實例中,每一個 DVM 實例都運行在一個獨立的進程空間中,獨立的進程可以防止在虛擬機崩潰的時候所有的程序都關閉。

4. DVM 由 Zygote 創建和初始化

Zygote 是第一個 DVM 進程,同時也用來創建和初始化 DVM 實例。每當系統需要創建一個應用程序時,Zygote 就會 fork 自身,快速的創建和初始化一個 DVM 實例,用于應用程序的運行。對于一些只讀的系統庫,所有的 DVM 實例都會和 Zygote 共享一塊內存區域,節省了內存開銷。

5. DVM 有共享機制

DVM 擁有預加載——共享的機制,不同的應用之間在運行時可以共享相同的類,擁有更高的效率。而 JVM 不存在這種共享機制,不同的程序,打包后彼此獨立,即便它們使用了同樣的類,運行時也都是單獨加載和運行的,無法進行共享。

6. DVM 早期沒有使用 JIT 編譯器

早期的 DVM 沒有使用 JIT 編譯器,每次執行代碼,都需要通過解釋器將 dex 代碼編譯成機器碼,然后執行,效率不高。為了解決這一問題,從 Android 2.2 版本開始 DVM 使用了 JIT 編譯器,它會對多次運行的代碼(熱點代碼)進行編譯,生成精簡的本地機器碼(Native Code),這樣在下次執行到相同代碼時,可以直接使用機器碼執行。但是,應用程序每次重新運行時,都需要做 JIT 編譯工作。

1.2 DVM 架構

DVM 源碼位于 dalvik/ 目錄下,Android 8.0 及 9.0 中的 DVM 源碼部分目錄說明如下:

目錄/文件 說明
dexdump 生成 dex 文件的反編譯查看工具,主要用來查看編譯出來的代碼的正確性和結構
dexgen dex 代碼生成器項目
docs DVM 相關幫助文檔
dx Java 字節碼轉換成 DVM 機器碼的工具
libdex 生成主機和設備處理 dex 文件的庫
tools 一些編譯和運行相關的工具
Android.mk 虛擬機編譯的 makefile 配置文件
MODULE_LICENSE_APACHE2 APACHE2 版權聲明文件
NOTICE 虛擬機源碼版權注意事項文件

其中,dalvik/libdex 會被編譯成 libdex.a 靜態庫,作為 dex 工具使用;dalvik/dexdump 是 .dex 文件的反編譯工具,DVM 架構如圖:


DVM架構

首先 Java 編譯器編譯的 .class 文件經過 dx 工具轉換為 .dex 文件,.dex 文件由類加載器處理,接著解釋器根據指令集對 Dalvik 字節碼進行解釋、執行,最后交由 Linux 處理。

1.3 DVM 的運行時堆

DVM運行時堆結構

DVM 的運行時堆使用標記——清除(Mark-Sweep)算法進行 GC,它由兩個 Space 以及多個輔助數據結構組成,兩個 Space 分別是 Zygote Space(Zygote Heap)和 Allocation Space(Active Heap)。Zygote Space 用來管理 Zygote 進程在啟動過程中預加載和創建的各種對象,Zygote Space 中不會觸發 GC,在 Zygote 進程和應用程序進程之間會共享 Zygote Space。在 Zygote 進程 fork 第一個子進程之前會把 Zygote Space 分為兩部分,原來的已經被使用的那部分堆仍稱為 Zygote Space,而未使用的那部分堆稱為 Allocation Space,以后的對象都會在 Allocation Space 上進行分配和釋放。Allocation Space 不是進程間共享的。除了這兩個 Space,還包含以下數據結構:

  • Card Table:用于 DVM Concurrent GC,當第一次進行垃圾回收標記后,記錄被標記對象信息。
  • Heap Bitmap:有兩個 Heap Bitmap,一個用來記錄上次 GC 存活的對象,另一個用來記錄這次 GC 存活的對象。
  • Mark Stack:DVM 的運行時堆使用標記——清除(Mark-Sweep)算法進行 GC,Mark Stack 就是在 GC 的標記階段使用的,用來遍歷存活的對象。

1.4 DVM 的 GC 日志

DVM 種的垃圾收集日志格式為:
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_status>, <Pause_time>
可以看出 DVM 的日志共有 5 個信息,其中 GC Reason 有多個。

1. 引起 GC 的原因

<GC_Reason> 就是引起 GC 的原因,有以下幾種:

  • GC_CONCURRENT:當堆開始填充時,并發 GC 釋放內存。
  • GC_FOR_MALLOC:當堆已滿時,App 嘗試分配內存而引起 GC,系統必須停止 App 并回收內存。
  • GC_HPROF_DUMP_HEAP:當請求創建 HPROF 文件來分析堆內存時出現 GC。
  • GC_EXPLICIT:顯式的 GC,例如調用 System.gc()(應避免顯式的調用 GC,信任 GC 會在需要時運行)。
  • GC_EXTERNAL_ALLOC:僅適用于 API 級別小于等于 10,且用于外部分配內存的 GC。
2. 其它的信息

除了引起 GC 的原因,其它信息如下:

  • Amount_freed:本次 GC 釋放內存的大小。
  • Heap_stats:堆的空閑內存百分比(已用內存)/(堆的總內存)。
  • External_memory_stats:API 小于等于級別 10 的內存分配(已分配的內存)/(引起 GC 的閾值)。
  • Pause_time:暫停時間,更大的堆會有更長的暫停時間。并發暫停時間會顯示兩個暫停時間,一個出現在垃圾收集開始時,另一個出現在在垃圾收集快要完成時。
3. 實例分析:

D/dalvikvm: GC_CONCURRENT freed 2011K, 60% free 3200K/8900K, external 4501K/5421K, paused 2ms+2ms
含義如下:
引起 GC 的原因時 GC_CONCURRENT;本次釋放的內存為 2011KB;堆的空閑百分比為 60%,已使用內存為 3200KB,堆的總內存為 8900KB;暫停的總時長為 4ms。

2. ART 虛擬機

ART(Android Runtime)虛擬機是 Android 4.4 發布的,用來替換 Dalvik 虛擬機,Android 4.4 默認采用 DVM,但是可以選擇使用 ART。在 Android 5.0 版本中默認使用 ART,DVM 從此退出歷史舞臺。

2.1 ART 與 DVM 的區別

主要有以下 4 點:

  1. DVM 中的應用每次運行時,字節碼搜需要通過 JIT 編譯器編譯成機器碼,這會使得應用程序的運行效率降低。而在 ART 中,系統在安裝應用程序時會進行一次 AOT(ahead of time compilation, 預編譯),將字節碼預先編譯成機器碼并存儲在本地,這樣應用程序每次運行時就不需要執行編譯了,運行 效率會大大提升,設備的耗電量也會降低。不過采用 AOT 也有缺點,主要有兩個:第一個是 AOT 會使得應用程序的安裝時間變長,尤其是一些復雜的應用;第二個是字節碼預先編譯成機器碼,機器碼需要的存儲空間會多一些。為了彌補以上兩個缺點,Android 7.0 版本的 ART 加入了即時編譯器 JIT,作為 AOT 的一個補充,在應用程序安裝時不會將字節碼全部編譯成機器碼,而是在運行種將熱點代碼編譯成機器碼,從而縮短了應用程序的安裝時間并節省了存儲空間。
  2. DVM 時為 32 位 CPU 設計的,而 ART 支持 64 位并兼容 32 位 CPU,這也是 DVM 被淘汰的主要原因之一。
  3. ART 對垃圾回收機制進行了改進,比如更頻繁地執行并行垃圾收集,將 GC 暫停由 2 次減少為 1 次等。
  4. ART 的運行時堆空間劃分與 DVM 不同。

2.2 ART 的運行時堆

與 DVM 的 GC 不同的是,ART 采用了多種垃圾回收方案,每個方案會運行不同的垃圾收集器,默認采用 CMS(Concurrent Mark-Sweep)方案,該方案主要使用了 sticky-CMS 和 partial-CMS。根據不同的 CMS 方案,ART 的運行時堆的空間也有不同的劃分,默認是由 4 個 Space 和多個輔助數據結構組成的,4 個 Space 分別是 Zygote Space、Allocation Space、Image Space 和 Large Object Space。Zygote Space、Allocation Space 和 DVM 中的作用是一樣的,Image Space 用來存放一些預加載類,Large Object Space 用來分配一些大對象(默認大小為 12KB),其中 Zygote Space 和 Image Space 是進程間共享的。采用標記——清除算法時的運行時堆空間劃分如圖:


DVM與ART運行時堆對比圖

除了這四個 Space,ART 的 Java 堆中還包括兩個 Mod Union Table,一個 Card Table,兩個 Heap Bitmap,兩個 Object Map,以及三個 Object Stack。

2.3 ART 的 GC 日志

ART 的 GC 日志與 DVM 不同,ART 會為那些主動請求的垃圾回收事件或者認為 GC 速度慢時才會打印 GC 日志。GC 速度慢指的是 GC 暫停超過 5ms 或者 GC 持續時間超過 100ms.如果 App 未處于可察覺的暫停進程狀態,那么它的 GC 不會被認為是慢速的。
ART 的 GC 日志具體格式為:
I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>
介紹如下:

1. 引起 GC 的原因

ART 的引起 GC 原因(GC_Reason)要比 DVM 多一些,有以下幾種:

  • Concurrent:并發 GC,不會使 App 的線程暫停,該 GC 是在后臺線程運行的,并不會阻止內存分配。
  • Alloc:當堆內存已滿時,App 嘗試分配內存而引起的 GC,這個 GC 會發生在正在分配內存的線程中。
  • Explicit:App 顯示的請求垃圾收集,例如調用 System.gc()。與 DVM 一樣,最佳做法是應該信任 GC 并避免顯式地請求 GC,顯示地請求 GC 會阻止分配線程并不必要地浪費 CPU 周期。如果顯式地請求 GC 導致其他線程被搶占,那么有可能會導致 jank(App 同一幀畫了多次)。
  • NativeAlloc:Native 內存分配時,比如為 Bitmaps 或者 RenderScript 分配對象,這會導致 Native 內存壓力,從而觸發 GC。
  • CollectorTransition:由堆轉換引起的回收,這是運行時切換 GC 而引起的。收集器轉換包括將所有對象從空閑列表空間復制到碰撞指針空間(反之亦然)。當前,收集器轉換僅在以下情況出現:在內存較小的設備上,App 將進程狀態從可察覺的暫停狀態變更為可察覺的非暫停狀態(反之亦然)。
  • HomogeneousSpaceCompact:齊性空間壓縮,指空閑列表到壓縮的空閑列表空間,通常發生在當 App 已經移動到可察覺的暫停進程狀態時。這樣做的主要原因是減少內存使用并對堆內存進行碎片整理。
  • DisableMovingGc:不是真正觸發 GC 的原因,發生在并發堆壓縮時,由于使用了 GetPrimitiveArrayCritical,收集會被阻塞。在一般情況下,強烈建議不要使用 GetPrimitiveArrayCritical,因為它在移動收集器方面有限制。
  • HeapTrim:不是觸發 GC 的原因,但是需要注意,收集會一直被阻塞,直到堆內存整理完畢。
2. 垃圾收集器名稱

GC_Name 指的是垃圾收集器名稱,有以下幾種:

  • Concurrent Mark Sweep(CMS):CMS 收集器是一種以獲取最短收集暫停時間為目標的收集器,采用標記——清楚算法實現。它是完整的垃圾收集器,能釋放除了 Image Space 外的所有的空間。
  • Concurrent Partial Mark Sweep:部分完整的垃圾收集器,能釋放除 Image Space 和 Zygote Space 外的所有空間。
  • Concurrent Sticky Mark Sweep:粘性收集器,基于分帶垃圾收集思想,只能釋放上次 GC 以來分配的對象。這個垃圾收集器比一個完整或者部分完整的垃圾收集器掃描的更頻繁,因為它更快而且有更短的暫停時間。
  • Marksweep + Semispace:非并發的 GC,復制 GC 用于堆轉換以及齊性空間壓縮(堆碎片整理)。
3. 其它信息
  • Object freed:本次 GC 從非 Large Object Space 中回收的對象數量。
  • Size_freed:本次 GC 從非 Large Object Space 中回收的字節數。
  • Large objects freed:本次 GC 從 Large Object Space 中回收的對象數量。
  • Large objects size freed:本次 GC 從 Large Object Space 中回收的字節數。
  • Heap stats:堆的空閑內存百分比,即(已用內存)/(堆的總內存)。
  • Pause times:暫停時間,暫停時間與在 GC 運行時修改的對象引用數量成比例。目前,ART 的 CMS 收集器僅有一次暫停,它出現在 GC 的結尾附近。有對象移動的垃圾收集器暫停時間會很長,會在大部分垃圾回收期間連續出現。
4. 實例分析

I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.21ms
這個 GC 日志的含義為引起 GC 原因是 Explicit; 垃圾收集器為 CMS 收集器;釋放對象的數量為 104710個,釋放字節數為 7MB;釋放大對象的數量為 21 個,釋放大對象字節數為 416KB;堆的空閑內存百分比為 33%,已用內存為 25MB,堆的總內存為 38MB;GC 暫停時長為 1.230ms,GC 總時長為 67.21ms。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容