理解Android虛擬機器體系結構

發表於2016-01-24

1 什麼是Dalvik虛擬機器

Dalvik是Google公司自己設計用於Android平臺的Java虛擬機器,它是Android平臺的重要組成部分,支援dex格式(Dalvik Executable)的Java應用程式的執行。dex格式是專門為Dalvik設計的一種壓縮格式,適合記憶體和處理器速度有限的系統。Google對其進行了特定的優化,使得Dalvik具有高效、簡潔、節省資源的特點。從Android系統架構圖知,Dalvik虛擬機器執行在Android的執行時庫層。

2 Dalvik虛擬機器的功能

Dalvik作為面向Linux、為嵌入式作業系統設計的虛擬機器,主要負責完成物件生命週期管理、堆疊管理、執行緒管理、安全和異常管理,以及垃圾回收等。Dalvik充分利用Linux程式管理的特定,對其進行了物件導向的設計,使得可以同時執行多個程式,而傳統的Java程式通常只能執行一個程式,這也是為什麼Android不採用JVM的原因。Dalvik為了達到優化的目的,底層的操作大多和系統核心相關,或者直接呼叫核心介面。另外,Dalvik早期並沒有JIT編譯器,直到Android2.2才加入了對JIT的技術支援。

3 Dalvik虛擬機器和Java虛擬機器的區別

本質上,Dalvik也是一個Java虛擬機器。但它特別之處在於沒有使用JVM規範。大多數Java虛擬機器都是基於棧的結構(詳情請參考:理解Java虛擬機器體系結構),而Dalvik虛擬機器則是基於暫存器。基於棧的指令很緊湊,例如,Java虛擬機器使用的指令只佔一個位元組,因而稱為位元組碼。基於暫存器的指令由於需要指定源地址和目標地址,因此需要佔用更多的指令空間。Dalvik虛擬機器的某些指令需要佔用兩個位元組。基於棧和基於暫存器的指令集各有優劣,一般而言,執行同樣的功能,前者需要更多的指令(主要是load和store指令),而後者需要更多的指令空間。需要更多指令意味著要多佔用CPU時間,而需要更多指令空間意味著資料緩衝(d-cache)更易失效。更多討論,虛擬機器隨談(一):直譯器,樹遍歷直譯器,基於棧與基於暫存器,大雜燴 給出了非常詳細的參考。

Java虛擬機器執行的是Java位元組碼,而Dalvik虛擬機器執行的是專有檔案格式dex。在Java程式中,Java類會被編譯成一個或多個class檔案,然後打包到jar檔案中,接著Java虛擬機器會從相應的class檔案和jar檔案中獲取對應的位元組碼。Android應用雖然也使用Java語言,但是在編譯成class檔案後,還會通過DEX工具將所有的class檔案轉換成一個dex檔案,Dalvik虛擬機器再從中讀取指令和資料。dex檔案除了減少整體的檔案尺寸和I/O操作次數,也提高了類的查詢速度。

由下圖可以看到,jar和apk檔案的組成結構,以及class檔案和dex檔案的差異。dex格式檔案使用共享的、特定型別的常量池機制來節省記憶體。常量池儲存類中的所有字面常量,它包括字串常量、欄位常量等值。

總的來說,Dalvik虛擬機器具有以下特點:

  • 使用dex格式的位元組碼,不相容Java位元組碼格式
  • 程式碼密度小,執行效率高,節省資源
  • 常量池只使用32位的索引
  • 有記憶體限制
  • 預設棧大小是12KB(3個頁,每頁4KB)
  • 堆預設啟動大小為2MB,預設最大值為16MB
  • 堆支援的最小啟動大小為1MB,支援的最大值為1024MB
  • 堆和棧引數可以通過-Xms和-Xmx修改

4 Dalvik系統結構

實際上,Dalvik是基於Apache Harmony(Apache軟體基金會的Java SE專案)的部分實現,提供了自己的一套庫,即上層Java應用程式編寫所使用的API。

以上圖示來自tech-insider。Apache Harmony大體上分為三個層:作業系統、Java虛擬機器、Java類庫。它的特點在於虛擬機器和類庫內部被高度模組化,每一個模組都有一定的介面定義。作業系統層與虛擬機器層之間的介面由Portability Layer定義,它封裝了不同作業系統的差異,為虛擬機器和類庫的原生程式碼提供了一套統一的API訪問底層系統呼叫。虛擬機器與類庫之間的介面除了Java規範定義的JNI、JVMITI外,還加入了一層虛擬機器介面,由核心類和原生程式碼組成。實現了虛擬機器介面的虛擬機器都可以使用Harmony的類庫實現,並且可以被Harmony提供的同一個Java啟動程式啟動。

下面是Dalvik虛擬機器的結構圖:

一個應用首先經過DX工具將class檔案轉換成Dalvik虛擬機器可以執行的dex檔案,然後由類載入器載入原生類和Java類,接著由直譯器根據指令集對Dalvik位元組碼進行解釋、執行。最後,根據dvm_arch引數選擇編譯的目標機體系結構。

4.1 dex檔案結構

dex檔案結構和class檔案結構差異的地方很多,但從攜帶的資訊上看,dex和class檔案是一致的。

  • header:儲存了各個資料型別的起始地址、偏移量等資訊。
  • proto_ids:描述函式原型資訊,包括返回值,引數資訊。比如“test:()V”
  • methods_ids:函式資訊,包括所屬類及對應的proto資訊。

更多dex格式的內容,Android安全–Dex檔案格式詳解 這篇文章進行了非常詳細的介紹。雖然dex檔案的結構很緊湊,但想要執行時的效能得到進一步提升,還需要對dex檔案進行進一步優化。優化主要針對以下幾個方面:

  • 調整所有欄位的位元組序和對齊結構中的每一個域
  • 驗證dex檔案中的所有類
  • 對一些特定的類進行優化,對方法裡的操作碼進行優化

dex檔案經過優化後檔案大小會膨脹,大約增加到原來的1~4倍。對於內建應用,一般在系統編譯後,便會生成優化檔案(odex: Optimized dex)。一個Android應用程式,需要經過以下過程才可以在Dalvik虛擬機器上執行:

  • 把Java原始檔編譯成class檔案
  • 使用DX工具把class檔案轉換成dex檔案
  • 使用aapt工具把dex檔案、資原始檔以及AndroidManifest.xml檔案(二進位制格式)組合成APK
  • 將APK安裝到Android裝置執行

上圖(來自網路)詳盡地展示了最終簽名後的APK是怎麼來的。

4.2 Dalvik類載入器

一個dex檔案需要類載入器載入原生類和Java類,然後通過直譯器根據指令集對Dalvik位元組碼進行解釋和執行。Dalvik類載入器使用mmap函式,將dex檔案對映到記憶體中,通過普通的記憶體讀取操作即可訪問dex檔案,然後解析dex檔案內容並載入其中的類到雜湊表中。

4.2.1 解析dex

總的來說,dex檔案可以抽象為三個部分:頭部、索引、資料。通過頭部可以知道索引的位置和數目,以及資料區的起始位置。將dex檔案對映到記憶體後,Dalvik會呼叫dexFileParse函式對其進行分析,分析的結果放到DexFile資料結構中。DexFile中的baseAddr指向對映區的起始位置,pClassDefs指向class索引的起始位置。為了加快class的查詢速度,還建立一個雜湊表,對class名字進行雜湊並生成索引。

4.2.2 載入class

解析工作完成後就進行class的載入,載入的類需要用ClassObject資料結構來儲存。

其中clazz指向ClassObject物件,還包含一個Lock物件。如果其它執行緒想要獲取它的鎖,只有等這個執行緒釋放。Dalvik每載入一個class都會對應一個ClassObject物件,載入過程會在記憶體中分配幾個區域,分別存放directMethod, virtualMethod, sfield, ifield。這些資訊從dex檔案的資料區中讀取。欄位Field的定義如下:

待得到class索引後,實際的載入由loadClassFromDex來完成。首先它會讀取class的具體資料,分別載入directMethod, virtualMethod, ifield和sfield,然後為ClassObject資料結構分配記憶體,並讀取dex檔案的相關資訊。載入完成後,將載入的class通過dvmAddClassToHash函式放入雜湊表,以方便下次查詢;最後,通過dvmLinkClass查詢該類的超類,如果有介面類則載入相應的介面類。

4.3 Dalvik直譯器

對於任何虛擬機器來說,直譯器無疑是核心的部分,所有的Java位元組碼都經過直譯器解釋執行。由於Dalvik直譯器的效率很重要,Android分別實現了C語言版和各種組合語言版的直譯器。直譯器通常是迴圈執行,需要一個入口函式呼叫處理程式執行第一條指令,而後每條指令執行時引出下一條指令,通過函式指標呼叫處理程式。

4.4 記憶體管理

垃圾收集是Dalvik虛擬機器記憶體管理的核心。此處只介紹Dalvik虛擬機器的垃圾收集功能。垃圾收集的效能在很大程度上影響了一個Java程式記憶體使用的效率。Dalvik虛擬機器使用常用的Mark-Sweep演算法,該演算法分Mark階段(標記出活動物件)、Sweep階段(回收垃圾記憶體)和可選的Compact階段(減少堆中的碎片)。Android記憶體管理原理  這篇文章講解得很詳細。

垃圾收集的第一步是標記出活動物件,因為沒有辦法識別那些不可訪問的物件,這樣所有未被標記的物件就是可以回收的垃圾。當進行垃圾收集時,需要停止Dalvik虛擬機器的執行(除垃圾收集外),因此垃圾收集又被稱作STW(stop-the-world)。Dalvik虛擬機器在執行過程中要維護一些狀態資訊,這些資訊包括:每個執行緒所儲存的暫存器、Java類中的靜態欄位、區域性和全域性的JNI引用,JVM中的所有函式呼叫會對應一個相應C的棧幀。每一個棧幀裡可能包含對物件的引用,比如包含物件引用的區域性變數和引數。所有這些引用資訊被加入到一個根集合中,然後從根集合開始,遞迴查詢可以從根集合出發訪問的物件。因此,Mark過程又叫做追蹤,追蹤所有可被訪問的物件。

垃圾收集的第二步就是回收記憶體。在Mark階段通過markBits點陣圖可以得到所有可訪問的物件集合,而liveBits點陣圖表示所有已經分配的物件集合。通過比較liveBits點陣圖和markBits點陣圖的差異就是所有可回收的物件集合。Sweep階段呼叫free來釋放這些記憶體給堆。

在底層記憶體實現上,Android系統使用的是msspace,這是一個輕量級的malloc實現。除了建立和初始化用於儲存普通Java物件的記憶體堆,Android還建立三個額外的記憶體堆:

  • “livebits”(用來存放堆上記憶體被佔用情況的點陣圖索引)
  • “markbits”(在GC時用於標註存活物件的點陣圖索引)
  • “markstack”(在GC中遍歷存活物件引用的標註棧)

虛擬機器通過一個名為gHs的全域性HeapSource變數來操控GC記憶體堆,而HeapSource裡通過heaps陣列可以管理多個堆(Heap),以滿足動態調整GC記憶體堆大小的要求。另外HeapSource裡還維護一個名為”livebits”的點陣圖索引,以跟蹤各個堆(Heap)的記憶體使用情況。剩下兩個資料結構”markstack”和”markbits”都是用在垃圾回收階段。

上圖中”livebits”維護堆上已用的記憶體資訊,而”markbits”這個點陣圖索引則指向存活的物件。 A、C、F、G、H物件需要保留,因此”markbits”分別指向他們(最後的H物件尚在標註過程中,因此沒有指標指向它)。而”markstack”就是在標註過程中跟蹤當前需要處理的物件要用到的標誌棧,此時其儲存了正在處理的物件F、G和H。

4.5 Dalvik的啟動流程

Dalvik程式管理是依賴於linux的程式體系結構的,如要為應用程式建立一個程式,它會使用linux的fork機制來複制一個程式。Zygote是一個虛擬機器程式,同時也是一個虛擬機器例項的孵化器,它通過init程式啟動。之前的文章有對此過程有詳細介紹:Android系統啟動分析(Init->Zygote->SystemServer->Home activity)。此處分析Dalvik虛擬機器啟動的相關過程。

AndroidRuntime類主要做了以下幾件事情:

  • 呼叫startVM建立一個Dalvik虛擬機器,JNI_CreateJavaVM真正建立並初始化虛擬機器例項
  • 呼叫startReg註冊Android核心類的JNI方法
  • 通過Zygote程式進入Java層

在JNI中,dvmCreateJNIEnv為當前執行緒建立和初始化一個JNI環境,即一個JNIEnvExt物件。最後呼叫dvmStartup來初始化前面建立的Dalvik虛擬機器例項。函式dvmInitZygote呼叫了系統的setpgid來設定當前程式,即Zygote程式的程式組ID。這一步完成後,Dalvik虛擬機器的建立和初始化工作就完成了。

5 Android的啟動

  • 啟動電源,載入載入程式到RAM
  • BootLoader引導
  • Linux Kernel啟動
  • Init程式建立
  • Init fork出Zygote程式,Zygote程式建立虛擬機器;建立系統服務
  • Android Home Launcher啟動

 

 

參考:

《Android技術內幕》

Dalvik虛擬機器簡要介紹和學習計劃

深入理解Android(二):Java虛擬機器Dalvik

dalvik虛擬記憶體管理之二——垃圾收集

Dalvik虛擬機器的啟動過程分析

相關文章