文章轉載至CSDN社區羅昇陽的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/8852432html
咱們知道,Android應用程序是運行在Dalvik虛擬機裏面的,而且每個應用程序對應有一個單獨的Dalvik虛擬機實例。除了指令集和類文件格 式不一樣,Dalvik虛擬機與Java虛擬機共享有差很少的特性,例如,它們都是解釋執行,而且支持即時編譯(JIT)、垃圾收集(GC)、Java本地 方法調用(JNI)和Java遠程調試協議(JDWP)等。本文對Dalvik虛擬機進行簡要介紹,以及制定學習計劃。java
老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!android
Dalvik虛擬機是由Dan Bornstein開發的,名字來源於他的祖先曾經居住過的位於冰島的同名小漁村。Dalvik虛擬機起源於Apache Harmony項目,後者是由Apache軟件基金會主導的,目標是實現一個獨立的、兼容JDK 5的虛擬機,並根據Apache License v2發佈。因而可知,Dalvik虛擬機從誕生的那一天開始,就和Java有說不清理不斷的關係。多線程
Dalvik虛擬機與Java虛擬機的最顯著區別是它們分別具備不一樣的類文件格式以及指令集。Dalvik虛擬機使用的是dex(Dalvik Executable)格式的類文件,而Java虛擬機使用的是class格式的類文件。一個dex文件能夠包含若干個類,而一個class文件只包括一 個類。因爲一個dex文件能夠包含若干個類,所以它就能夠將各個類中重複的字符串和其它常數只保存一次,從而節省了空間,這樣就適合在內存和處理器速度有 限的手機系統中使用。通常來講,包含有相同類的未壓縮dex文件稍小於一個已經壓縮的jar文件。併發
Dalvik虛擬機使用的指令是基於寄存器的,而Java虛擬機使用的指令集是基於堆棧的。基於堆棧的指令很緊湊,例如,Java虛擬機使用的指令只佔一 個字節,於是稱爲字節碼。基於寄存器的指令因爲須要指定源地址和目標地址,所以須要佔用更多的指令空間,例如,Dalvik虛擬機的某些指令須要佔用兩個 字節。基於堆棧和基於寄存器的指令集各有優劣,通常而言,執行一樣的功能,前者須要更多的指令(主要是load和store指令),然後者須要更多的指令 空間。須要更多指令意味着要多佔用CPU時間,而須要更多指令空間意味着數據緩衝(d-cache)更易失效。oracle
此外,還有一種觀點認爲,基於堆棧的指令更具可移植性,由於它不對目標機器的寄存器進行任何假設。然而,基於寄存器的指令因爲對目標機器的寄存器進行了假設,所以,它更有利於進行AOT(ahead- of-time)優化。 所謂AOT,就是在解釋語言程序運行以前,就先將它編譯成本地機器語言程序。AOT本質上是一種靜態編譯,它是是相對於JIT而言的,也就是說,前者是在 程序運行前進行編譯,然後者是在程序運行時進行編譯。運行時編譯意味着能夠利用運行時信息來獲得比較靜態編譯更優化的代碼,同時也意味不能進行某些高級優 化,由於優化過程太耗時了。另外一方面,運行前編譯因爲不佔用程序運行時間,所以,它就能夠不計時間成原本優化代碼。不管AOT,仍是JIT,最終的目標都 是將解釋語言編譯爲本地機器語言,而本地機器語言都是基於寄存器來執行的,所以,在某種程度來說,基於寄存器的指令更有利於進行AOT編譯以及優化。app
事實上,基於寄存器和基於堆棧的指令集之爭,就如精簡指令集(RISC)和複雜指令集(CISC)之爭,誰優誰劣,至今是沒有定論的。例如,上面提到完成 相同的功能,基於堆棧的Java虛擬機須要更多的指令,所以就會比基於寄存器的Dalvik虛擬機慢,然而,在2010年,Oracle在一個ARM設備 上使用一個non-graphical Java benchmarks來對比Java SE Embedded和Android 2.2的性能,發現後者比前者慢了2~3倍。上述性能比較結論以及數據能夠參考如下兩篇文章:框架
1. Virtual Machine Showdown: Stack Versus Registerside
2. Java SE Embedded Performance Versus Android 2.2函數
基於寄存器的Dalvik虛擬機和基於堆棧的Java虛擬機的更多比較和分析,還能夠參考如下文章:
1. http://en.wikipedia.org/wiki/Dalvik_(software)
2. http://www.infoq.com/news/2007/11/dalvik
3. http://www.zhihu.com/question/20207106
無論結論如何,Dalvik虛擬機都在盡最大的努力來優化自身,這些措施包括:
1. 將多個類文件收集到同一個dex文件中,以便節省空間;
2. 使用只讀的內存映射方式加載dex文件,以即可以多進程共享dex文件,節省程序加載時間;
3. 提早調整好字節序(byte order)和字對齊(word alignment)方式,使得它們更適合於本地機器,以便提升指令執行速度;
4. 儘可能提早進行字節碼驗證(bytecode verification),提升程序的加載速度;
5. 須要重寫字節碼的優化要提早進行。
這些優化措施的更具體描述能夠參考Dalvik Optimization and Verification With dexopt一文。
分析完Dalvik虛擬機和Java虛擬機的區別以後,接下來咱們再簡要分析一下Dalvik虛擬機的其它特性,包括內存管理、垃圾收集、JIT、JNI以及進程和線程管理。
一. 內存管理
Dalvik虛擬機的內存大致上能夠分爲Java Object Heap、Bitmap Memory和Native Heap三種。
Java Object Heap是用來分配Java對象的,也就是咱們在代碼new出來的對象都是位於Java Object Heap上的。Dalvik虛擬機在啓動的時候,能夠經過-Xms和-Xmx選項來指定Java Object Heap的最小值和最大值。爲了不Dalvik虛擬機在運行的過程當中對Java Object Heap的大小進行調整而影響性能,咱們能夠經過-Xms和-Xmx選項來將它的最小值和最大值設置爲相等。
Java Object Heap的最小和最大默認值爲2M和16M,可是手機在出廠時,廠商會根據手機的配置狀況來對其進行調整,例如,G一、Droid、Nexus One和Xoom的Java Object Heap的最大值分別爲16M、24M、32M 和48M。咱們能夠經過ActivityManager類的成員函數getMemoryClass來得到Dalvik虛擬機的Java Object Heap的最大值。
ActivityManager類的成員函數getMemoryClass的實現以下所示:
這個函數定義在文件frameworks/base/core/java/android/app/ActivityManager.java中。
Dalvik虛擬機在啓動的時候,就是經過讀取系統屬性dalvik.vm.heapsize的值來得到Java Object Heap的最大值的,而ActivityManager類的成員函數getMemoryClass最終也經過讀取這個系統屬性的值來得到Java Object Heap的最大值。
這個Java Object Heap的最大值也就是咱們平時所說的Android應用程序進程可以使用的最大內存。這裏必需要注意的是,Android應用程序進程可以使用的最大內存指的是可以用來分配Java Object的堆。
Bitmap Memory也稱爲External Memory,它是用來處理圖像的。在HoneyComb以前,Bitmap Memory是在Native Heap中分配的,可是這部份內存一樣計入Java Object Heap中,也就是說,Bitmap佔用的內存和Java Object佔用的內存加起來不能超過Java Object Heap的最大值。這就是爲何咱們在調用BitmapFactory相關的接口來處理大圖像時,會拋出一個OutOfMemoryError異常的原 因:
在HoneyComb以及更高的版本中,Bitmap Memory就直接是在Java Object Heap中分配了,這樣就能夠直接接受GC的管理。
Native Heap就是在Native Code中使用malloc等分配出來的內存,這部份內存是不受Java Object Heap的大小限制的,也就是它能夠自由使用,固然它是會受到系統的限制。可是有一點須要注意的是,不要由於Native Heap能夠自由使用就濫用,由於濫用Native Heap會致使系統可用內存急劇減小,從而引起系統採起激進的措施來Kill掉某些進程,用來補充可用內存,這樣會影響系統體驗。
此外,在HoneyComb以及更高的版本中,咱們能夠在AndroidManifest.xml的application標籤中增長一個值等於 「true」的android:largeHeap屬性來通知Dalvik虛擬機應用程序須要使用較大的Java Object Heap。事實上,在內存受限的手機上,即便咱們將一個應用程序的android:largeHeap屬性設置爲「true」,也是不能增長它可用的 Java Object Heap的大小的,而即使是能夠經過這個屬性來增大Java Object Heap的大小,通常狀況也不該該使用該屬性。爲了提升系統的總體體驗,咱們須要作的是致力於下降應用程序的內存需求,而不是增長增長應用程序的Java Object Heap的大小,畢竟系統總共可用的內存是固定的,一個應用程序用得多了,就意味意其它應用程序用得少了。
二. 垃圾收集(GC)
Dalvik虛擬機能夠自動回收那些再也不使用了的Java Object,也就是那些再也不被引用了的Java Object。垃圾自動收集機制將開發者從內存問題中解放出來,極大地提升了開發效率,以及提升了程序的可維護性。
咱們知道,在C或者C++中,開發者須要手動地管理在堆中分配的內存,可是這每每致使不少問題。例如,內存分配以後忘記釋放,形成內存泄漏。又如,非法訪 問那些已經釋放了的內存,引起程序崩潰。若是沒有一個好的C或者C++應用程序開發框架,通常的開發者根本沒法駕馭內存問題,由於程序大了以後,很容易造 成失控。最要命的是,內存被破壞的時候,並不必定就是程序崩潰的時候,它就是一顆不定時炸彈,說不許何時會被引爆,所以,查找緣由是很是困難的。
從這裏咱們也能夠推斷出,Android爲何會選擇Java而不是C/C++來做來應用程序開發語言,就是爲了可以讓開發遠離內存問題,而將精力集中在 業務上,開發出更多更好的APP來,從而迎頭趕超iOS。固然,Android系統內存也存在大量的C/C++代碼,這隻要考慮性能問題,畢竟C/C++ 程序的運行性能總體上仍是優於運行在虛擬機之上的Java程序的。不過,爲了不出現內存問題,在Android系統內部的C++代碼碼,大量地使用了智能指針來自動管理對象的生命週期。選擇Java來做爲Android應用程序的開發語言,能夠說是技術與商業之間一個折衷,事實證實,這種折衷是成功的。
回到正題,在GingerBread以前,Dalvik虛擬使用的垃圾收集機制有如下特色:
1. Stop-the-word,也就是垃圾收集線程在執行的時候,其它的線程都中止;
2. Full heap collection,也就是一次收集徹底部的垃圾;
3. 一次垃圾收集形成的程序停止時間一般都大於100ms。
在GingerBread以及更高的版本中,Dalvik虛擬使用的垃圾收集機制獲得了改進,以下所示:
1. Cocurrent,也就是大多數狀況下,垃圾收集線程與其它線程是併發執行的;
2. Partial collection,也就是一次可能只收集一部分垃圾;
3. 一次垃圾收集形成的程序停止時間一般都小於5ms。
Dalvik虛擬機執行完成一次垃圾收集以後,咱們一般能夠看到相似如下的日誌輸出:
在這一行日誌中,GC_CONCURRENT表示GC緣由,2049K表示總共回收的內存,3571K/9991K表示Java Object Heap統計,即在9991K的Java Object Heap中,有3571K是正在使用的,4703K/5261K表示External Memory統計,即在5261K的External Memory中,有4703K是正在使用的,2ms+2ms表示垃圾收集形成的程序停止時間。
三. 即時編譯(JIT)
前面提到,JIT是相對AOT而言的,即JIT是在程序運行的過程當中進行編譯的,而AOT是在程序運行前進行編譯的。在程序運行的過程當中進行編譯既有好 處,也有壞處。好處在於能夠利用程序的運行時信息來對編譯出來的代碼進行優化,而壞處在於佔用程序的運行時間,也就是說不能花太多時間在代碼編譯和優化之 上。
爲了解決時間問題,JIT可能只會選擇那些熱點代碼進行編譯或者優化。根據2-8原則,一個程序80%的時間可能都是在重複執行20%的代碼。所以,JIT就能夠選擇這20%常常執行的代碼來進行編譯和優化。
爲了充分地利用好運行時信息來優化代碼,JIT採用一種激進的方法。JIT在編譯代碼的時候,會對程序的運行狀況進行假設,而且按照這種假設來對代碼進行 優化。隨着程序的代碼,若是前面的假設一直保持成立,那麼JIT就什麼也不用作,所以就能夠提升程序的運行性能。一旦前面的假設再也不成立了,那麼JIT就 須要對前面編譯優化的代碼進行調整,以便適應新的狀況。這種調整成本多是很昂貴的,可是隻要假設不成立的狀況不多或者幾乎不會發生,那麼得到的好處仍是 大於壞處的。因爲JIT在編譯和優化代碼的時候,對程序的運行狀況進行了假設,所以,它所採起的激進優化措施又稱爲賭博,即Gambling。
咱們以一個例子來講明這種Gambling。咱們知道,Java的同步原語涉及到Lock和Unlock操做。Lock和Unlock操做是很是耗時的, 並且它們只有在多線程環境中才真的須要。可是一些同步函數或者同步代碼,有程序運行的時候,有可能始終都是被單線程執行,也就是說,這些同步函數或者同步 代碼不會被多線程同時執行。這時候JIT就能夠採起一種Lazy Unlocking機制。
當一個線程T1進入到一個同步代碼C時,它仍是按照正常的流程來獲取一個輕量級鎖L1,而且線程T1的ID會記錄在輕量鎖L1上。當經程T1離開同步函數 或者同步代碼時,它並不會釋放前面得到的輕量級鎖L1。當線程T1再次進入同步代碼C時,它就會發現輕量級鎖L的全部者正是本身,所以,它就能夠直接執行 同步代碼C。這時候若是另一個線程T2也要進入同步代碼C,它就會發現輕量級鎖L已經被線程T1獲取。在這種狀況下,JIT就須要檢查線程T1的調用堆 棧,看看它是否還在執行同步代碼C。若是是的話,那麼就須要將輕量級鎖L1轉換成一個重量級鎖L2,而且將重量級鎖L2的狀態設置爲鎖定,而後再讓線程 T2在重量級鎖L2上睡眠。等線程T1執行完成同步代碼C以後,它就會按照正常的流程來釋放重量級鎖L2,從而喚醒線程T2來執行同步代碼C。另外一方面, 若是線程T2在進入同步代碼C的時候,JIT經過檢查線程T1的調用堆棧,發現它已經離開同步代碼C了,那麼它就直接將輕量級鎖L1的全部者記錄爲線程 T2,而且讓線程T2執行同步代碼C。
經過上述的Lazy Unlocking機制,咱們就能夠充分地利用程序的運行時信息來提升程序的執行性能,這種優化對於靜態編譯的語言來講,是沒法作到的。從這個角度來看, 咱們就能夠說,靜態編譯語言(如C++)並不必定比在虛擬機上執行的語言(如Java)快,這是由於後者能夠有一種強大的武器叫作JIT。
Dalvik虛擬機從Android 2.2版本開始,才支持JIT,並且是可選的。在編譯Dalvik虛擬機的時候,能夠經過WITH_JIT宏來將JIT也編譯進去,而在啓動Dalvik虛擬機的時候,能夠經過-Xint:jit選項來開啓JIT功能。
關於虛擬機JIT的實現原理的簡要介紹,能夠進一步參考這篇文章:http://blog.reverberate.org/2012/12/hello-jit-world-joy-of-simple-jits.html。
四. Java本地調用(JNI)
不管如何,虛擬機最終都是運行在目標機器之上的,也就是說,它須要將本身的指令翻譯成目標機器指令來執行,而且有些功能,須要經過調用目標機器運行的操做 系統接口來完成。這樣就須要有一個機制,使得函數調用能夠從Java層穿越到Native層,也就是C/C++層。這種機制就稱爲Java本地調用,即 JNI。固然,咱們在執行Native代碼的時候,有時候也是須要調用到Java函數的,這一樣是能夠經過JNI機制來實現。也就是說,JNI機制既支持 在Java函數中調用C/C++函數,也支持在C/C++函數中調用Java函數。
事實上,Dalvik虛擬機提供的Java運行時庫,大部分都是經過調用目標機器操做系統接口來實現的,也就是經過調用Linux系統接口來實現的。例 如,當咱們調用android.os.Process類的成員函數start來建立一個進程的時候,最終會調用到Linux系統提供的fork系統調用來 建立一個進程。
同時,爲了方便開發者使用C/C++語言來開發應用程序,Android官方提供了NDK。經過NDK,咱們就可使用JNI機制來在Java函數中調用 到C/C++函數。不過Android官方是不提倡使用NDK來開發應用程序的,這從它對NDK的支持遠遠不如SDK的支持就能夠看得出來。
五. 進程和線程管理
通常來講,虛擬機的進程和線程都是與目標機器本地操做系統的進程和線程一一對應的,這樣作的好處是可使本地操做系統來調度進程和線程。進程和線程調度是 操做系統的核心模塊,它的實現是很是複雜的,特別是考慮到多核的狀況,所以,就徹底沒有必要在虛擬機中提供一個進程和線程庫。
Dalvik虛擬機運行在Linux操做系統之上。咱們知道,Linux操做系統並無純粹的線程概念,只要兩個進程共享同一個地址空間,那麼就能夠認爲 它們同一個進程的兩個線程。Linux操做系統提供了兩個fork和clone兩個調用,其中,前者就是用來建立進程的,然後者就是用來建立線程的。關於 Linux操做系統的進程和線程的實現,能夠參考在前面Android學習啓動篇一文中提到的經典Linux內核書籍。
關於Android應用程序進程,它有兩個很大的特色,下面咱們就簡要介紹一下。
第一個特色是每個Android應用程序進程都有一個Dalvik虛擬機實例。這樣作的好處是Android應用程序進程之間不會相互影響,也就是說,一個Android應用程序進程的意外停止,不會影響到其它的Android應用程序進程的正常運行。
第二個特色是每個Android應用程序進程都是由一種稱爲Zygote的進程fork出來的。Zygote進程是由init進程啓動起來的,也就是在 系統啓動的時候啓動的。Zygote進程在啓動的時候,會建立一個虛擬機實例,而且在這個虛擬機實例將全部的Java核心庫都加載起來。每當Zygote 進程須要建立一個Android應用程序進程的時候,它就經過複製自身來實現,也就是經過fork系統調用來實現。這些被fork出來的Android應 用程序進程,一方面是複製了Zygote進程中的虛擬機實例,另外一方面是與Zygote進程共享了同一套Java核心庫。這樣不只Android應用程序 進程的建立過程很快,並且因爲全部的Android應用程序進程都共享同一套Java核心庫而節省了內存空間。
關於Dalvik虛擬機的特性,咱們就簡要介紹到這裏。事實上,Dalvik虛擬機和Java虛擬機的實現是相似的,例如,Dalvik虛擬機也支持 JDWP(Java Debug Wire Protocol)協議,這樣咱們就可使用DDMS來調試運行在Dalvik虛擬機中的進程。對Dalvik虛擬機的其它特性或者實現原理有興趣的,建 議均可以參考Java虛擬機的實現,這裏提供三本參考書:
1. Java Virtual Machine Specification (Java SE 7)
2. Inside the Java Virtual Machine, Second Edition
3. Oracle JRockit: The Definitive Guide
另外,關於Dalvik虛擬機的指令集和dex文件格式的介紹,能夠參考官方文檔:http://source.android.com/tech/dalvik/index.html。若是對虛擬機的實現原理有興趣的,還能夠參考這個連接:http://www.weibo.com/1595248757/zvdusrg15。
在這裏,咱們學習Dalvik虛擬機的目標是打通Java層到C/C++層之間的函數調用,從而能夠更好地理解Android應用程序是如何在Linux內核上面運行的。爲了達到這個目的,在接下來的文章中,咱們將關注如下四個情景:
1. Dalvik虛擬機的啓動過程;
2. Dalvik虛擬機的運行過程;
3. JNI函數的註冊過程;
4. Java進程和線程的建立過程。
掌握了這四個情景以後,再結合前面的全部文章,咱們就能夠從上到下地打通整個Android系統了,敬請關注!
老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!