1. 程式人生 > >理解Android虛擬機體系結構

理解Android虛擬機體系結構

棧管理 運行時 映射 ble 緩沖 odex () UNC 工具

理解Android虛擬機體系結構
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的技術支持。
想學習更多Android知識,或者獲取相關資料請加入Android技術開發交流2群:862625886。本群可免費獲取Gradle、RxJava、小程序、Hybrid、移動架構、NDK、React Native、性能優化等技術教程!

3 Dalvik虛擬機和Java虛擬機的區別
本質上,Dalvik也是一個Java虛擬機。但它特別之處在於沒有使用JVM規範。大多數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

技術分享圖片

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文件中的所有類
  • 對一些特定的類進行優化,對方法裏的操作碼進行優化
      dex文件經過優化後文件大小會膨脹,大約增加到原來的1~4倍。對於內置應用,一般在系統編譯後,便會生成優化文件(odex: Optimized dex)。一個Android應用程序,需要經過以下過程才可以在Dalvik虛擬機上運行:

  • 把Java源文件編譯成class文件
  • 使用DX工具把class文件轉換成dex文件
  • 使用aapt工具把dex文件、資源文件以及AndroidManifest.xml文件(二進制格式)組合成APK
  • 將APK安裝到Android設備運行
    技術分享圖片

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數據結構來存儲
typedef struct Object {<br/>ClassObject* clazz; // 類型對象<br/>Lock lock; // 鎖對象<br/>} Object;
其中clazz指向ClassObject對象,還包含一個Lock對象。如果其它線程想要獲取它的鎖,只有等這個線程釋放。Dalvik每加載一個class都會對應一個ClassObject對象,加載過程會在內存中分配幾個區域,分別存放directMethod, virtualMethod, sfield, ifield。這些信息從dex文件的數據區中讀取。字段Field的定義如下:

struct Field {
    ClassObject* clazz;    //所屬類型
    const char* name;      // 變量名稱
    const char* signature; // 如“Landroid/os/Debug;”
    u4 accessFlags;        // 訪問標記

    #ifdef PROFILE_FIELD_ACCESS
        u4 gets;
        u4 puts;
    #endif
};

待得到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階段(減少堆中的碎片)

垃圾收集的第一步是標記出活動對象,因為沒有辦法識別那些不可訪問的對象,這樣所有未被標記的對象就是可以回收的垃圾。當進行垃圾收集時,需要停止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進程啟動。此處分析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知識,或者獲取相關資料請加入Android技術開發交流2群:862625886。本群可免費獲取Gradle、RxJava、小程序、Hybrid、移動架構、NDK、React Native、性能優化等技術教程!

理解Android虛擬機體系結構