Google於2007年末正式發佈了Android SDK,Dalvik虛擬機也第一次進入了人們的視野。它的做者是丹.伯恩斯坦(Dan Bornstein)。Dalvik虛擬機做爲Android平臺的核心組件,擁有以下幾個特色:java
體積小,佔用內存空間小;shell
專有的DEX可執行文件格式,體積更小,執行速度更快;安全
常量池採用32位索引值,尋址類方法名,字段名,常量更快;數據結構
基於寄存器架構,並擁有一套完整的指令系統;架構
提供了對象生命週期管理,堆棧管理,線程管理,安全和異常管理以及垃圾回收等重要功能;框架
全部的Android程序都運行在Android系統進程裏,每一個進程對應着一個Dalvik虛擬機實例。socket
1. Dalvik虛擬機與Java虛擬機的區別函數
Dalvik虛擬機與傳統的Java虛擬機有着許多不一樣點,二者並不兼容,它們顯著的不一樣點主要表如今如下幾個方面:工具
Java虛擬機運行的是Java字節碼,Dalvik虛擬機運行的是Dalvik字節碼。傳統的Java程序通過編譯,生成Java字節碼保存在class文件中,Java虛擬機經過解碼class文件中的內容來運行程序。而Dalvik虛擬機運行的是Dalvik字節碼,全部的Dalvik字節碼由Java字節碼轉換而來,並被打包到一個DEX(Dalvik Executable)可執行文件中。Dalvik虛擬機經過解釋DEX文件來執行這些字節碼。測試
Dalvik可執行文件體積小。Android SDK中有一個叫dx的工具負責將Java字節碼轉換爲Dalvik字節碼。dx工具對Java類文件從新排列,消除在類文件中出現的全部冗餘信息,避免虛擬機在初始化時出現反覆的文件加載與解析過程。通常狀況下,Java類文件中包含多個不一樣的方法簽名,若是其餘的類文件引用該類文件中的方法,方法簽名也會被複制到其類文件中,也就是說,多個不一樣的類會同時包含相同的方法簽名,一樣地,大量的字符串常量在多個類文件中也被重複使用。這些冗餘信息會直接增長文件的體積,同時也會嚴重影響虛擬機解析文件的效率。消除其中的冗餘信息,從新組合造成一個常量池,全部的類文件共享同一個常量池。因爲dx工具對常量池的壓縮,使得相同的字符串,常量在DEX文件中只出現一次,從而減少了文件的體積。
Java虛擬機與Dalvik虛擬機架構不一樣。Java虛擬機基於棧架構,程序在運行時虛擬機須要頻繁的從棧上讀取或寫入數據,這個過程須要更多的指令分派與內存訪問次數,會耗費很多CPU時間,對於像手機設備資源有限的設備來講,這是至關大的一筆開銷。Dalvik虛擬機基於寄存器架構。數據的訪問經過寄存器間直接傳遞,這樣的訪問方式比基於棧方式要快不少。測試代碼以下:
package t1; public class Hello { public static void main(String[] args) { Hello hello = new Hello(); System.out.println(hello.foo(5, 3)); } public int foo(int a,int b) { return (a + b) * (a - b); } }
將以上內容保存爲Hello.java。打開命令提示符,執行命令:
$ javac -source 1.6 -target 1.6 Hello.java
注:若是使用1.7及以上版本的JDK編譯Hello.java,生成Hello.class默認的版本會比較低,使用dx生成dex文件會提示class文件無效。解決方法是強制指定class文件的版本。
繼續上面的討論,執行上面的命令生成Hello.class文件。而後執行命令:
./dx --dex --output=t1/Hello.dex t1/Hello.class
執行上面的命令前,命令行進入到dx工具所在的目錄(位於Android SDK的platform-tools目錄中),再把t1/Hello.class目錄與文件copy到dx工具所在的目錄,而後再執行上面的命令生成dex文件。
接下來在Hello.class所在目錄使用javap反編譯Hello.class查看foo()函數的Java字節碼,執行如下命令:
$ javap -c -cp . Hello.class
命令執行後獲得以下代碼(foo()函數部分):
public int foo(int, int); Code: 0: iload_1 1: iload_2 2: iadd 3: iload_1 4: iload_2 5: isub 6: imul 7: ireturn
下面再使用dexdump(位於Android sdk的platform-tools目錄中)查看foo()函數的Dalvik字節碼,執行如下命令:
$ ./dexdump -d t1/Hello.dex
命令執行後整理輸出結果,能夠獲得以下代碼(foo()函數部分):
000198: |[000198] t1.Hello.foo:(II)I 0001a8: 9000 0304 |0000: add-int v0, v3, v4 0001ac: 9101 0304 |0002: sub-int v1, v3, v4 0001b0: b210 |0004: mul-int/2addr v0, v1 0001b2: 0f00 |0005: return v0
查看上面的Java字節碼,發現foo()函數一共佔用了8個字節,代碼中每條指令佔用1個字節。比起Java虛擬機字節碼,上面的Dalvik字節碼顯得簡潔不少,只有4條指令就完成了下面的操做。
經過上面的分析 能夠發現,基於寄存器架構的Dalvik虛擬機與基於棧架構的Java虛擬機相比,因爲生成的代碼指令減小了,程序執行速度會更快一些。
2. Dalvik虛擬機是如何執行程序的
Android系統由Linux內核,函數庫,Android運行時,應用程序框架以及應用程序組成。Dalvik虛擬機屬行Android運行時環境,它與一些核心庫共同承擔Android應用程序的運行工做。
Android系統啓動加載完內核後,第一個執行的是init進程,init進程首先要作的是設備的初始化工做,而後讀取inic.rc文件並啓動系統中的重要外部程序 Zygote。Zygote進程是Android全部進程的孵化器進程,它啓動後會首先初始化Dalvik虛擬機,而後啓動system_server並進入Zygote模式,經過socket等候命令。當執行一個Android應用程序時,system_server進程經過Binder IPC方式發送命令給Zygote,Zygote收到命令後經過fork自身建立一個Dalvik虛擬機的實例來執行應用程序的入口函數,這樣一個程序就啓動完成了。
Zygote提供了三種建立進程的方法:
fork(),建立一個Zygote進程(這種方式實際不會被調用)
forkAndSpecialize(),建立一個非Zygote進程
forkSystemServer(),建立一個系統服務進程。
其中,Zygote進程能夠再fork()出其餘進程,非Zygote進程則不能fork其餘進程,而系統服務進程在終止後它的子進程也必終止。當進程fork成功後,執行的工做就交給了Dalvik虛擬機。Dalvik虛擬機首先經過loadClassFromDex()函數完成類的裝載工做,每一個類被成功解析後都會擁有一個ClassObject類型的數據結構存儲在運行時環境中,虛擬機使用gDvm.loadedClasses全局哈希表來存儲與查詢全部裝載進來的類,隨後,字節碼驗證器使用dvmVerifyCodeFlow()函數對裝入的代碼進行校驗,接着虛擬機調用FindCass()函數查找並裝載main方法類,隨後調用dvmInterpret()函數初始化解釋器並執行字節碼流。
3. 關於Dalvik虛擬機JIT(即時編譯)
JIT(Just-in-time Compilation,即時編譯),又稱爲動態編譯,是一種經過在運行時將字節碼翻譯爲機器碼的技術,使得程序的執行速度更快。Android2.2版本系統的Dalvik虛擬機引入了JIT技術,官方宣稱新版的Dalvik虛擬機比以往執行速度快3~6倍。主流的JIT包含兩種字節碼編譯方式:
method方式:以函數或方法爲單位進行編譯。
trace方式:以trace爲單位進行編譯。
method方式很好理解,那什麼是trace方式呢?在函數中通常不多是順序執行代碼的,多數的代碼都分紅了好幾條執行路徑,其中函數的有些路徑在實際運行過程當中是不多被執行的,這部分路徑被稱爲「冷路徑」,而執行比較頻繁的路徑被稱爲「熱路徑」。採用傳統的method方式會編譯整個方法的代碼,這會使得在「冷路徑」上浪費不少編譯時間,而且耗費更多的內存;trace方法編譯則可以快速地獲取「熱路徑」代碼,使用更短的時間與更少的內存來編譯代碼。
目前,Dalvik虛擬機默認採用trace方式編譯代碼,同時也支持採用method方式來編譯。