一、發(fā)展簡述
1.1 初期——Dalvik
??在 Android 系統(tǒng)初期,不同于 Java 平臺使用 JVM 加載字節(jié)碼文件(.class),Android 系統(tǒng)由 Dalvik 擔任虛擬機的角色,每次運行程序的時候,Dalvik 負責加載 dex/odex 文件并解析成機器碼交由系統(tǒng)調(diào)用。Dalvik虛擬機是基于apache的java虛擬機,并被改進以適應低內(nèi)存,低處理器速度的移動設備環(huán)境。
1.2 引入JIT
??為了適應硬件速度的提升,Android 系統(tǒng)系統(tǒng)也在不斷更新,單一的 Dalvik 虛擬機已經(jīng)漸漸地滿足系統(tǒng)的要求了,2010 年 5 月 20 日,Google 發(fā)布 Android 2.2(Froyo凍酸奶),在這個版本中,Google 在 Android 虛擬中加入了 JIT 編譯器(Just-In-Time Compiler)。
??Dalvik 使用 JIT 進行即時編譯,借助 Java HotSpot VM,JIT 編譯器可以對執(zhí)行次數(shù)頻繁的 dex/odex 代碼進行編譯與優(yōu)化,將 dex/odex 中的 Dalvik Code(Smali 指令集)翻譯成相當精簡的 Native Code 去執(zhí)行,JIT 的引入使得 Dalvik 的性能提升了 3~6 倍。
1.3 ART 誕生
??2013 年 10 月 31 日,Google 發(fā)布 Android 4.4(奇巧Kitkat),帶來了全新的虛擬機運行環(huán)境 ART(Android RunTime)的預覽版和全新的編譯策略 AOT(Ahead-of-time),這個時候 ART 是和 Dalvik 共存的,用戶可以在兩者之間進行選擇。
??2014 年 10 月 16 日,Google 發(fā)布 Android 5.0(棒棒糖Lollipop),ART 全面取代 Dalvik 成為 Android 虛擬機運行環(huán)境,至此,Dalvik 退出歷史舞臺,AOT 也成為唯一的編譯模式。
??AOT 和 JIT 的不同之處在于:JIT 是在運行時進行編譯,是動態(tài)編譯,并且每次運行程序的時候都需要對 odex 重新進行編譯;而 AOT 是靜態(tài)編譯,應用在安裝的時候會啟動 dex2oat 過程把 dex 預編譯成 ELF 文件,每次運行程序的時候不用重新編譯,是真正意義上的本地應用。
1.4 JIT回歸
??在 Android 5.x 和 6.x 的機器上,系統(tǒng)每次 OTA 升級完成重啟的時候都會有個應用優(yōu)化的過程,這個過程就是剛才所說的 dex2oat 過程,這個過程比較耗時并且會占用額外的存儲空間。
??2016 年 8 月 22 日,Google 發(fā)布 Android 7.0(牛軋?zhí)荖ougat),JIT 編譯器回歸,形成 AOT/JIT 混合編譯模式,這種混合編譯模式的特點是:
??應用在安裝的時候 dex 不會被編譯
??應用在運行時 dex 文件先通過解析器(Interpreter)后會被直接執(zhí)行(這一步驟跟 Android 2.2 - Android 4.4之前的行為一致),與此同時,熱點函數(shù)(Hot Code)會被識別并被 JIT 編譯后存儲在 jit code cache 中并生成 profile 文件以記錄熱點函數(shù)的信息。
手機進入 IDLE(空閑) 或者 Charging(充電) 狀態(tài)的時候,系統(tǒng)會掃描 App 目錄下的 profile 文件并執(zhí)行 AOT 過程進行編譯。
??可以看出,混合編譯模式綜合了 AOT 和 JIT 的各種優(yōu)點,使得應用在安裝速度加快的同時,運行速度、存儲空間和耗電量等指標都得到了優(yōu)化。
二、Dalvik和傳統(tǒng)JVM的區(qū)別
??嚴格來說,Dalvik并不是java虛擬機,它并沒有遵循JVM的實現(xiàn)規(guī)范。Dalvik與JVM在以下幾個方面有明顯不同:
- 架構(gòu)
??java虛擬機基于棧,基于棧的機器必須使用指令來載入和操作棧上數(shù)據(jù);Dalvik虛擬機基于寄存器。 - 字節(jié)碼
??java虛擬機運行的是java字節(jié)碼。(java類會被編譯成一個或多個字節(jié)碼.class文件,打包到.jar文件中,java虛擬機從相應的.class文件和.jar獲取相應的字節(jié)碼)。Dalvik運行的是自己專屬的.dex字節(jié)碼格式。(java類被編譯成.class文件后,會通過一個dx工具將所有的.class文件轉(zhuǎn)換成一個.dex文件,然后Dalvik虛擬機會從其中讀取指令和數(shù)據(jù)) - 進程模型
??JVM設計上是一個JVM運行多個java程序?qū)嵗?,然而DVM允許每個應用程序允許在自己獨立的DVM中,每個DVM運行在獨立的進程空間,放在了某個應用崩潰的時候影響到其他應用進程。 - 共享機制
??DVM有預加載-共享的機制,不用應用之間在運行時可以共享相同的類,擁有更高的效率。JVM不存在這種共享機制,不同的程序打包后是獨立的。 - DVM早期沒有使用JIT編譯
??JVM使用了JIT編譯器,而DVM早期沒有使用JIT編譯器。早期的DVM每次執(zhí)行代碼,都需要通過解釋器將dex代碼編譯成機器碼,然后交給系統(tǒng)處理,效率不是很高。從Android 2.2版本開始,DVM使用了JIT編譯器,它會對多次運行的代碼(熱點代碼)進行編譯,生成相當精簡的本地機器碼(Native Code),這樣在下次執(zhí)行到相同邏輯的時候,直接使用編譯之后的本地機器碼,而不是每次都需要編譯。但是應用程序每一次重新運行的時候,都要重做這個編譯工作,因此每次重新打開應用程序,都需要JIT編譯。
三、DVM和ART的區(qū)別
??ART是一種在Android操作系統(tǒng)上的運行環(huán)境,ART能夠在第一次安裝的時候,把應用程序的字節(jié)碼轉(zhuǎn)換為機器碼。采用了預編譯(AOT,Ahead-Of-Time)技術。DVM和ART的區(qū)別主要有4點:
- JIT與AOT
??DVM中的應用每次運行時,字節(jié)碼都需要通過JIT編譯器編譯為機器碼,這會使得應用程序的運行效率降低。而在ART中,系統(tǒng)在安裝應用程序時會進行一次AOT(ahead of time compilation,預編譯),將字節(jié)碼預先編譯成機器碼并存儲在本地,這樣應用程序每次運行時就不需要執(zhí)行編譯了,運行效率會大大提升,設備的耗電量也會降低。
??采用AOT也會有缺點,主要有兩個:第一個是AOT會使得應用程序的安裝時間變長,尤其是一些復雜的應用,第二個是字節(jié)碼預先編譯成機器碼,機器碼需要的存儲空間會多一些。為了解決上面的缺點,Android 7.0版本中的ART加入了即時編譯器JIT,作為AOT的一個補充,在應用程序安裝時并不會將字節(jié)碼全部編譯成機器碼,而是在運行中將熱點代碼編譯成機器碼,從而縮短應用程序的安裝時間并節(jié)省了存儲空間。 - 處理位數(shù)
??DVM是為32位CPU設計的,而ART支持64位并兼容32位CPU,這也是DVM被淘汰的主要原因之一。 - 垃圾回收
??ART對垃圾回收機制進行了改進,比如更頻繁地執(zhí)行并行垃圾收集,將GC暫停由2次減少為1次等。 - 內(nèi)存劃分
??ART的運行時堆空間劃分和DVM不同。
由此可以總結(jié)出ART的優(yōu)缺點:

四、垃圾回收
4.1 Dalvik垃圾回收
??Dalvik虛擬機比其他Java虛擬機中的垃圾收集要簡單一些, 因為沒有進行內(nèi)存整理(no compacting)。也就是說堆內(nèi)存中的對象在創(chuàng)建之后其地址永遠都不會發(fā)生改變, 使得虛擬機其余部分的實現(xiàn)變得相對簡單。
4.1.1 DVM運行時堆
???Android 4.x中DVM運行時堆使用的是標記-清除算法進行GC。DVM的運行時堆包括兩個Space和一些輔助數(shù)據(jù)結(jié)構(gòu):

4.1.2 引起DVM發(fā)生GC的原因
??有以下幾種:
- GC_CONCURRENT:當堆開始填充時,并發(fā)GC可以釋放內(nèi)存。
2 .GC_POR_MALLOC:當堆內(nèi)存已滿時,App嘗試分配內(nèi)存而引起的GC,系統(tǒng)必須停止App并回收內(nèi)存。
GC_HPROF_DUMP_HEAP:當請求創(chuàng)建HPROF文件來分析堆內(nèi)存時出現(xiàn)的GC。
GC_EXPLICIT:顯式的GC,例如調(diào)用System.gc()(應該避免調(diào)用顯式的GC,信任GC會在需要時運行)。
GC_EXTERNAL_ALLOC:僅適用于API級別小于等于10,且用于外部分配內(nèi)存的GC。
4.2 ART垃圾回收
???與DVM的GC不同的是,ART 采用了多種垃圾收集方案,每個方案會運行不同的垃圾收集器,默認是采用了CMS(Concurrent Mark-Sweep)方案,該方案主要使用了sticky-CMS和partial-CMS。根據(jù)不同的CMS方案,ART的運行時堆的空間也會有不同的劃分。
4.2.1 ART運行時堆
??默認的ART運行時堆由4個Space和多個輔助結(jié)構(gòu)組成:

??采用標記-清除算法時。DVM和ART的空間劃分如下:

4.2.2 引起ART發(fā)生GC的原因
??有以下幾種:
- Concurrent:并發(fā)GC,不會使App的線程暫停,該GC是在后臺線程運行的,并不會阻止內(nèi)存分配。
- Alloc:當堆內(nèi)存已滿時,App嘗試分配內(nèi)存而引起的GC,這個GC會發(fā)生在正在分配內(nèi)存的線程中。
- Explicit:App顯示的請求垃圾收集,例如調(diào)用System.gc()。與DVM一樣,最佳做法是應該信任GC并避免顯式地請求GC,顯式地請求GC會阻止分配線程并不必要地浪費CPU周期。如果顯式地請求GC導致其他線程被搶占,那么有可能會導致jank(App同一幀畫了多次)。
- NativeAlloc:Native內(nèi)存分配時,比如為Bitmaps或者RenderScript分配對象,這會導致Native內(nèi)存壓力,從而觸發(fā)GC。
- CollectorTransition:由堆轉(zhuǎn)換引起的回收,這是運行時切換GC而引起的。收集器轉(zhuǎn)換包括將所有對象從空閑列表空間復制到碰撞指針空間(反之亦然)。當前,收集器轉(zhuǎn)換僅在以下情況下出現(xiàn):在內(nèi)存較小的設備上,App 將進程狀態(tài)從可察覺的暫停狀態(tài)變更為可察覺的非暫停狀態(tài)(反之亦然)。
- HomogeneousSpaceCompact:齊性空間壓縮是指空閑列表到壓縮的空閑列表空間,通常發(fā)生在當App已經(jīng)移動到可察覺的暫停進程狀態(tài)時。這樣做的主要原因是減少了內(nèi)存使用并對堆內(nèi)存進行碎片整理。
- DisableMovingGc:不是真正觸發(fā)GC的原因,發(fā)生并發(fā)堆壓縮時,由于使用了GetPrimitiveArrayCritical,收集會被阻塞。在一般情況下,建議不要使用GetPrimitiveArrayCritical,因為它在移動收集器方面具有限制。
- HcapTrim:不是觸發(fā)GC的原因,但是請注意,收集會一直被阻塞,直到堆內(nèi)存整理完畢。
五、小結(jié)
??本文簡要描述了安卓中虛擬機的發(fā)展,其實目前來看,ART的名字是非常貼切的,DVM開始具備了非虛擬機的特點,而ART則完全無法僅僅用虛擬機去描述,ART為安卓應用提供了一整套的運行時環(huán)境,而不是作為虛擬機去執(zhí)行指令。