理解Android虛擬機體系結(jié)構(gòu)
1. 什么是Dalvik虛擬機
Dalvik是Google公司自己設(shè)計用于Android平臺的Java虛擬機,它是Android平臺的重要組成部分,支持dex格式(Dalvik Executable)的Java應(yīng)用程序的運行。dex格式是專門為Dalvik設(shè)計的一種壓縮格式,適合內(nèi)存和處理器速度有限的系統(tǒng)。Google對其進行了特定的優(yōu)化,使得Dalvik具有高效、簡潔、節(jié)省資源的特點。從Android系統(tǒng)架構(gòu)圖知,Dalvik虛擬機運行在Android的運行時庫層。
2. Dalvik虛擬機的功能
Dalvik作為面向Linux、為嵌入式操作系統(tǒng)設(shè)計的虛擬機,主要負責(zé)完成對象生命周期管理、堆棧管理、線程管理、安全和異常管理,以及垃圾回收等。Dalvik充分利用Linux進程管理的特定,對其進行了面向?qū)ο蟮脑O(shè)計,使得可以同時運行多個進程,而傳統(tǒng)的Java程序通常只能運行一個進程,這也是為什么Android不采用JVM的原因。Dalvik為了達到優(yōu)化的目的,底層的操作大多和系統(tǒng)內(nèi)核相關(guān),或者直接調(diào)用內(nèi)核接口。另外,Dalvik早期并沒有JIT編譯器,直到Android2.2才加入了對JIT的技術(shù)支持。
3. Dalvik虛擬機和Java虛擬機的區(qū)別
本質(zhì)上,Dalvik也是一個Java虛擬機。但它特別之處在于沒有使用JVM規(guī)范。大多數(shù)Java虛擬機都是基于棧的結(jié)構(gòu)(詳情請參考:理解Java虛擬機體系結(jié)構(gòu)),而Dalvik虛擬機則是基于寄存器?;跅5闹噶詈芫o湊,例如,Java虛擬機使用的指令只占一個字節(jié),因而稱為字節(jié)碼?;诩拇嫫鞯闹噶钣捎谛枰付ㄔ吹刂泛湍繕?biāo)地址,因此需要占用更多的指令空間。Dalvik虛擬機的某些指令需要占用兩個字節(jié)?;跅:突诩拇嫫鞯闹噶罴饔袃?yōu)劣,一般而言,執(zhí)行同樣的功能,前者需要更多的指令(主要是load和store指令),而后者需要更多的指令空間。需要更多指令意味著要多占用CPU時間,而需要更多指令空間意味著數(shù)據(jù)緩沖(d-cache)更易失效。更多討論,虛擬機隨談(一):解釋器,樹遍歷解釋器,基于棧與基于寄存器,大雜燴 給出了非常詳細的參考。
Java虛擬機運行的是Java字節(jié)碼,而Dalvik虛擬機運行的是專有文件格式dex。在Java程序中,Java類會被編譯成一個或多個class文件,然后打包到j(luò)ar文件中,接著Java虛擬機會從相應(yīng)的class文件和jar文件中獲取對應(yīng)的字節(jié)碼。Android應(yīng)用雖然也使用Java語言,但是在編譯成class文件后,還會通過DEX工具將所有的class文件轉(zhuǎn)換成一個dex文件,Dalvik虛擬機再從中讀取指令和數(shù)據(jù)。dex文件除了減少整體的文件尺寸和I/O操作次數(shù),也提高了類的查找速度。
由下圖可以看到,jar和apk文件的組成結(jié)構(gòu),以及class文件和dex文件的差異。dex格式文件使用共享的、特定類型的常量池機制來節(jié)省內(nèi)存。常量池存儲類中的所有字面常量,它包括字符串常量、字段常量等值。
總的來說,Dalvik虛擬機具有以下特點:
- 使用dex格式的字節(jié)碼,不兼容Java字節(jié)碼格式
- 代碼密度小,運行效率高,節(jié)省資源
- 常量池只使用32位的索引
- 有內(nèi)存限制
- 默認棧大小是12KB(3個頁,每頁4KB)
- 堆默認啟動大小為2MB,默認***值為16MB
- 堆支持的最小啟動大小為1MB,支持的***值為1024MB
- 堆和棧參數(shù)可以通過-Xms和-Xmx修改
4. Dalvik系統(tǒng)結(jié)構(gòu)
實際上,Dalvik是基于Apache Harmony(Apache軟件基金會的Java SE項目)的部分實現(xiàn),提供了自己的一套庫,即上層Java應(yīng)用程序編寫所使用的API。
以上圖示來自tech-insider。Apache Harmony大體上分為三個層:操作系統(tǒng)、Java虛擬機、Java類庫。它的特點在于虛擬機和類庫內(nèi)部被高度模塊化,每一個模塊都有一定的接口定義。操作系統(tǒng)層與虛擬機層之間的接口由Portability Layer定義,它封裝了不同操作系統(tǒng)的差異,為虛擬機和類庫的本地代碼提供了一套統(tǒng)一的API訪問底層系統(tǒng)調(diào)用。虛擬機與類庫之間的接口除了Java規(guī)范定義的JNI、JVMITI外,還加入了一層虛擬機接口,由內(nèi)核類和本地代碼組成。實現(xiàn)了虛擬機接口的虛擬機都可以使用Harmony的類庫實現(xiàn),并且可以被Harmony提供的同一個Java啟動程序啟動。
下面是Dalvik虛擬機的結(jié)構(gòu)圖:
一個應(yīng)用首先經(jīng)過DX工具將class文件轉(zhuǎn)換成Dalvik虛擬機可以執(zhí)行的dex文件,然后由類加載器加載原生類和Java類,接著由解釋器根據(jù)指令集對Dalvik字節(jié)碼進行解釋、執(zhí)行。***,根據(jù)dvm_arch參數(shù)選擇編譯的目標(biāo)機體系結(jié)構(gòu)。
4.1 dex文件結(jié)構(gòu)
dex文件結(jié)構(gòu)和class文件結(jié)構(gòu)差異的地方很多,但從攜帶的信息上看,dex和class文件是一致的。
- header:存儲了各個數(shù)據(jù)類型的起始地址、偏移量等信息。
- proto_ids:描述函數(shù)原型信息,包括返回值,參數(shù)信息。比如“test:()V”
- methods_ids:函數(shù)信息,包括所屬類及對應(yīng)的proto信息。
更多dex格式的內(nèi)容,Android安全–Dex文件格式詳解 這篇文章進行了非常詳細的介紹。雖然dex文件的結(jié)構(gòu)很緊湊,但想要運行時的性能得到進一步提升,還需要對dex文件進行進一步優(yōu)化。優(yōu)化主要針對以下幾個方面:
- 調(diào)整所有字段的字節(jié)序和對齊結(jié)構(gòu)中的每一個域
- 驗證dex文件中的所有類
- 對一些特定的類進行優(yōu)化,對方法里的操作碼進行優(yōu)化
dex文件經(jīng)過優(yōu)化后文件大小會膨脹,大約增加到原來的1~4倍。對于內(nèi)置應(yīng)用,一般在系統(tǒng)編譯后,便會生成優(yōu)化文件(odex: Optimized dex)。一個Android應(yīng)用程序,需要經(jīng)過以下過程才可以在Dalvik虛擬機上運行:
- 把Java源文件編譯成class文件
- 使用DX工具把class文件轉(zhuǎn)換成dex文件
- 使用aapt工具把dex文件、資源文件以及AndroidManifest.xml文件(二進制格式)組合成APK
- 將APK安裝到Android設(shè)備運行
上圖(來自網(wǎng)絡(luò))詳盡地展示了最終簽名后的APK是怎么來的。
4.2 Dalvik類加載器
一個dex文件需要類加載器加載原生類和Java類,然后通過解釋器根據(jù)指令集對Dalvik字節(jié)碼進行解釋和執(zhí)行。Dalvik類加載器使用mmap函數(shù),將dex文件映射到內(nèi)存中,通過普通的內(nèi)存讀取操作即可訪問dex文件,然后解析dex文件內(nèi)容并加載其中的類到哈希表中。
4.2.1 解析dex
總的來說,dex文件可以抽象為三個部分:頭部、索引、數(shù)據(jù)。通過頭部可以知道索引的位置和數(shù)目,以及數(shù)據(jù)區(qū)的起始位置。將dex文件映射到內(nèi)存后,Dalvik會調(diào)用dexFileParse函數(shù)對其進行分析,分析的結(jié)果放到DexFile數(shù)據(jù)結(jié)構(gòu)中。DexFile中的baseAddr指向映射區(qū)的起始位置,pClassDefs指向class索引的起始位置。為了加快class的查找速度,還創(chuàng)建一個哈希表,對class名字進行哈希并生成索引。
4.2.2 加載class
解析工作完成后就進行class的加載,加載的類需要用ClassObject數(shù)據(jù)結(jié)構(gòu)來存儲。
- typedef struct Object {
- ClassObject* clazz; // 類型對象
- Lock lock; // 鎖對象
- } Object;
其中clazz指向ClassObject對象,還包含一個Lock對象。如果其它線程想要獲取它的鎖,只有等這個線程釋放。Dalvik每加載一個class都會對應(yīng)一個ClassObject對象,加載過程會在內(nèi)存中分配幾個區(qū)域,分別存放directMethod, virtualMethod, sfield, ifield。這些信息從dex文件的數(shù)據(jù)區(qū)中讀取。字段Field的定義如下:
- struct Field {
- ClassObject* clazz; //所屬類型
- const char* name; // 變量名稱
- const char* signature; // 如“Landroid/os/Debug;”
- u4 accessFlags; // 訪問標(biāo)記
- #ifdef PROFILE_FIELD_ACCESS
- u4 gets;
- u4 puts;
- #endif
- };
待得到class索引后,實際的加載由loadClassFromDex來完成。首先它會讀取class的具體數(shù)據(jù),分別加載directMethod, virtualMethod, ifield和sfield,然后為ClassObject數(shù)據(jù)結(jié)構(gòu)分配內(nèi)存,并讀取dex文件的相關(guān)信息。加載完成后,將加載的class通過dvmAddClassToHash函數(shù)放入哈希表,以方便下次查找;***,通過dvmLinkClass查找該類的超類,如果有接口類則加載相應(yīng)的接口類。
4.3 Dalvik解釋器
對于任何虛擬機來說,解釋器無疑是核心的部分,所有的Java字節(jié)碼都經(jīng)過解釋器解釋執(zhí)行。由于Dalvik解釋器的效率很重要,Android分別實現(xiàn)了C語言版和各種匯編語言版的解釋器。解釋器通常是循環(huán)執(zhí)行,需要一個入口函數(shù)調(diào)用處理程序執(zhí)行***條指令,而后每條指令執(zhí)行時引出下一條指令,通過函數(shù)指針調(diào)用處理程序。
4.4 內(nèi)存管理
垃圾收集是Dalvik虛擬機內(nèi)存管理的核心。此處只介紹Dalvik虛擬機的垃圾收集功能。垃圾收集的性能在很大程度上影響了一個Java程序內(nèi)存使用的效率。Dalvik虛擬機使用常用的Mark-Sweep算法,該算法分Mark階段(標(biāo)記出活動對象)、Sweep階段(回收垃圾內(nèi)存)和可選的Compact階段(減少堆中的碎片)。Android內(nèi)存管理原理 這篇文章講解得很詳細。
垃圾收集的***步是標(biāo)記出活動對象,因為沒有辦法識別那些不可訪問的對象,這樣所有未被標(biāo)記的對象就是可以回收的垃圾。當(dāng)進行垃圾收集時,需要停止Dalvik虛擬機的運行(除垃圾收集外),因此垃圾收集又被稱作STW(stop-the-world)。Dalvik虛擬機在運行過程中要維護一些狀態(tài)信息,這些信息包括:每個線程所保存的寄存器、Java類中的靜態(tài)字段、局部和全局的JNI引用,JVM中的所有函數(shù)調(diào)用會對應(yīng)一個相應(yīng)C的棧幀。每一個棧幀里可能包含對對象的引用,比如包含對象引用的局部變量和參數(shù)。所有這些引用信息被加入到一個根集合中,然后從根集合開始,遞歸查找可以從根集合出發(fā)訪問的對象。因此,Mark過程又叫做追蹤,追蹤所有可被訪問的對象。
垃圾收集的第二步就是回收內(nèi)存。在Mark階段通過markBits位圖可以得到所有可訪問的對象集合,而liveBits位圖表示所有已經(jīng)分配的對象集合。通過比較liveBits位圖和markBits位圖的差異就是所有可回收的對象集合。Sweep階段調(diào)用free來釋放這些內(nèi)存給堆。
在底層內(nèi)存實現(xiàn)上,Android系統(tǒng)使用的是msspace,這是一個輕量級的malloc實現(xiàn)。除了創(chuàng)建和初始化用于存儲普通Java對象的內(nèi)存堆,Android還創(chuàng)建三個額外的內(nèi)存堆:
- “livebits”(用來存放堆上內(nèi)存被占用情況的位圖索引)
- “markbits”(在GC時用于標(biāo)注存活對象的位圖索引)
- “markstack”(在GC中遍歷存活對象引用的標(biāo)注棧)
虛擬機通過一個名為gHs的全局HeapSource變量來操控GC內(nèi)存堆,而HeapSource里通過heaps數(shù)組可以管理多個堆(Heap),以滿足動態(tài)調(diào)整GC內(nèi)存堆大小的要求。另外HeapSource里還維護一個名為”livebits”的位圖索引,以跟蹤各個堆(Heap)的內(nèi)存使用情況。剩下兩個數(shù)據(jù)結(jié)構(gòu)”markstack”和”markbits”都是用在垃圾回收階段。
上圖中”livebits”維護堆上已用的內(nèi)存信息,而”markbits”這個位圖索引則指向存活的對象。 A、C、F、G、H對象需要保留,因此”markbits”分別指向他們(***的H對象尚在標(biāo)注過程中,因此沒有指針指向它)。而”markstack”就是在標(biāo)注過程中跟蹤當(dāng)前需要處理的對象要用到的標(biāo)志棧,此時其保存了正在處理的對象F、G和H。
4.5 Dalvik的啟動流程
Dalvik進程管理是依賴于linux的進程體系結(jié)構(gòu)的,如要為應(yīng)用程序創(chuàng)建一個進程,它會使用linux的fork機制來復(fù)制一個進程。Zygote是一個虛擬機進程,同時也是一個虛擬機實例的孵化器,它通過init進程啟動。之前的文章有對此過程有詳細介紹:Android系統(tǒng)啟動分析(Init->Zygote->SystemServer->Home activity)。此處分析Dalvik虛擬機啟動的相關(guān)過程。
AndroidRuntime類主要做了以下幾件事情:
- 調(diào)用startVM創(chuàng)建一個Dalvik虛擬機,JNI_CreateJavaVM真正創(chuàng)建并初始化虛擬機實例
- 調(diào)用startReg注冊Android核心類的JNI方法
- 通過Zygote進程進入Java層
在JNI中,dvmCreateJNIEnv為當(dāng)前線程創(chuàng)建和初始化一個JNI環(huán)境,即一個JNIEnvExt對象。***調(diào)用dvmStartup來初始化前面創(chuàng)建的Dalvik虛擬機實例。函數(shù)dvmInitZygote調(diào)用了系統(tǒng)的setpgid來設(shè)置當(dāng)前進程,即Zygote進程的進程組ID。這一步完成后,Dalvik虛擬機的創(chuàng)建和初始化工作就完成了。
5. Android的啟動
- 啟動電源,加載引導(dǎo)程序到RAM
- BootLoader引導(dǎo)
- Linux Kernel啟動
- Init進程創(chuàng)建
- Init fork出Zygote進程,Zygote進程創(chuàng)建虛擬機;創(chuàng)建系統(tǒng)服務(wù)
- Android Home Launcher啟動
參考:
- 《Android技術(shù)內(nèi)幕》
- Dalvik虛擬機簡要介紹和學(xué)習(xí)計劃
(http://blog.csdn.net/luoshengyang/article/details/8852432)
- 深入理解Android(二):Java虛擬機Dalvik
(http://www.infoq.com/cn/articles/android-in-depth-dalvik)
- dalvik虛擬內(nèi)存管理之二——垃圾收集
(http://www.miui.com/thread-75028-1-1.html)
- Dalvik虛擬機的啟動過程分析
(http://blog.csdn.net/luoshengyang/article/details/8885792)