Java 虛擬機(jī)在前面的系列文章中有所介紹(http://www.lxweimin.com/c/84fea797b420),Android 所使用的虛擬機(jī)(Dalvik 和 ART)與 JVM 有所不同,這篇文章來(lái)介紹一下。
1. Dalvik 虛擬機(jī)
Dalvik 虛擬機(jī)(Dalvik Virtual Machine),簡(jiǎn)稱 Dalvik VM 或者 DVM。它是 Google 專門(mén)為 Android 平臺(tái)開(kāi)發(fā)的虛擬機(jī),運(yùn)行在 Android 運(yùn)行時(shí)庫(kù)中。DVM 并不是一個(gè) Java 虛擬機(jī),原因在下面介紹。
1.1 DVM 與 JVM 的區(qū)別
DVM 之所以不是一個(gè) JVM,主要原因是 DVM 并沒(méi)有遵循 JVM 規(guī)范來(lái)實(shí)現(xiàn),與 JVM 主要區(qū)別如下:
1. 基于的架構(gòu)不同
JVM 的執(zhí)行的指令是基于棧結(jié)構(gòu),這就意味著需要去棧中讀寫(xiě)數(shù)據(jù),所需的指令會(huì)很多,會(huì)導(dǎo)致速度變慢,對(duì)于性能有限的移動(dòng)設(shè)備,顯然不合適。DVM 是基于寄存器的,沒(méi)有基于棧的虛擬機(jī)在復(fù)制數(shù)據(jù)時(shí)使用的大量的出入棧指令,同時(shí)指令更緊湊、更簡(jiǎn)介。但是由于指定了操作數(shù),所以指令會(huì)比基于棧的指令大,但是由于指令數(shù)量的減少,總的代碼不會(huì)增加多少。
2. 執(zhí)行的字節(jié)碼不同
在 Java SE 程序中,Java 類被編譯成一個(gè)或多個(gè) .class 文件,并打包成 jar 文件,而后 JVM 會(huì)通過(guò)相應(yīng)的 .class 文件和 jar 文件獲取對(duì)相應(yīng)的字節(jié)碼。執(zhí)行順序?yàn)椋?java 文件 → .class 文件 → .jar 文件,而 DVM 會(huì)用 dx 工具將所有的 .class 文件轉(zhuǎn)換為一個(gè) .dex 文件,然后 DVM 會(huì)從該 .dex 文件讀取指令和數(shù)據(jù)。執(zhí)行順序?yàn)椋?java 文件 → .class 文件 → .dex 文件。
jar 文件和 apk 文件結(jié)構(gòu)如圖:
關(guān)于 class 文件結(jié)構(gòu)的詳細(xì)說(shuō)明,可參考:
當(dāng) JVM 加載 .jar 文件的時(shí)候,會(huì)加載里面所有的 .class 文件,這種加載方式對(duì)于性能有限的移動(dòng)設(shè)備不合適。在 .apk 文件中,一般情況下只包含一個(gè) .dex 文件,這個(gè) .dex 文件把所有的 .class 文件信息整合在一起,這樣就提升了加載速度。.class 文件種也會(huì)存在一些冗余信息,dex 工具會(huì)去除冗余信息,并把所有的 .class 文件整合到 .dex 文件種,減少 I/O 操作,加快了類的查找速度。
3. DVM 允許在有限的內(nèi)存中同時(shí)運(yùn)行多個(gè)進(jìn)程
DVM 經(jīng)過(guò)優(yōu)化,允許在有限的內(nèi)存中同時(shí)運(yùn)行多個(gè)進(jìn)程。在 Android 中的每一個(gè)應(yīng)用都運(yùn)行在一個(gè) DVM 實(shí)例中,每一個(gè) DVM 實(shí)例都運(yùn)行在一個(gè)獨(dú)立的進(jìn)程空間中,獨(dú)立的進(jìn)程可以防止在虛擬機(jī)崩潰的時(shí)候所有的程序都關(guān)閉。
4. DVM 由 Zygote 創(chuàng)建和初始化
Zygote 是第一個(gè) DVM 進(jìn)程,同時(shí)也用來(lái)創(chuàng)建和初始化 DVM 實(shí)例。每當(dāng)系統(tǒng)需要?jiǎng)?chuàng)建一個(gè)應(yīng)用程序時(shí),Zygote 就會(huì) fork 自身,快速的創(chuàng)建和初始化一個(gè) DVM 實(shí)例,用于應(yīng)用程序的運(yùn)行。對(duì)于一些只讀的系統(tǒng)庫(kù),所有的 DVM 實(shí)例都會(huì)和 Zygote 共享一塊內(nèi)存區(qū)域,節(jié)省了內(nèi)存開(kāi)銷。
5. DVM 有共享機(jī)制
DVM 擁有預(yù)加載——共享的機(jī)制,不同的應(yīng)用之間在運(yùn)行時(shí)可以共享相同的類,擁有更高的效率。而 JVM 不存在這種共享機(jī)制,不同的程序,打包后彼此獨(dú)立,即便它們使用了同樣的類,運(yùn)行時(shí)也都是單獨(dú)加載和運(yùn)行的,無(wú)法進(jìn)行共享。
6. DVM 早期沒(méi)有使用 JIT 編譯器
早期的 DVM 沒(méi)有使用 JIT 編譯器,每次執(zhí)行代碼,都需要通過(guò)解釋器將 dex 代碼編譯成機(jī)器碼,然后執(zhí)行,效率不高。為了解決這一問(wèn)題,從 Android 2.2 版本開(kāi)始 DVM 使用了 JIT 編譯器,它會(huì)對(duì)多次運(yùn)行的代碼(熱點(diǎn)代碼)進(jìn)行編譯,生成精簡(jiǎn)的本地機(jī)器碼(Native Code),這樣在下次執(zhí)行到相同代碼時(shí),可以直接使用機(jī)器碼執(zhí)行。但是,應(yīng)用程序每次重新運(yùn)行時(shí),都需要做 JIT 編譯工作。
1.2 DVM 架構(gòu)
DVM 源碼位于 dalvik/ 目錄下,Android 8.0 及 9.0 中的 DVM 源碼部分目錄說(shuō)明如下:
目錄/文件 | 說(shuō)明 |
---|---|
dexdump | 生成 dex 文件的反編譯查看工具,主要用來(lái)查看編譯出來(lái)的代碼的正確性和結(jié)構(gòu) |
dexgen | dex 代碼生成器項(xiàng)目 |
docs | DVM 相關(guān)幫助文檔 |
dx | Java 字節(jié)碼轉(zhuǎn)換成 DVM 機(jī)器碼的工具 |
libdex | 生成主機(jī)和設(shè)備處理 dex 文件的庫(kù) |
tools | 一些編譯和運(yùn)行相關(guān)的工具 |
Android.mk | 虛擬機(jī)編譯的 makefile 配置文件 |
MODULE_LICENSE_APACHE2 | APACHE2 版權(quán)聲明文件 |
NOTICE | 虛擬機(jī)源碼版權(quán)注意事項(xiàng)文件 |
其中,dalvik/libdex 會(huì)被編譯成 libdex.a 靜態(tài)庫(kù),作為 dex 工具使用;dalvik/dexdump 是 .dex 文件的反編譯工具,DVM 架構(gòu)如圖:
首先 Java 編譯器編譯的 .class 文件經(jīng)過(guò) dx 工具轉(zhuǎn)換為 .dex 文件,.dex 文件由類加載器處理,接著解釋器根據(jù)指令集對(duì) Dalvik 字節(jié)碼進(jìn)行解釋、執(zhí)行,最后交由 Linux 處理。
1.3 DVM 的運(yùn)行時(shí)堆
DVM 的運(yùn)行時(shí)堆使用標(biāo)記——清除(Mark-Sweep)算法進(jìn)行 GC,它由兩個(gè) Space 以及多個(gè)輔助數(shù)據(jù)結(jié)構(gòu)組成,兩個(gè) Space 分別是 Zygote Space(Zygote Heap)和 Allocation Space(Active Heap)。Zygote Space 用來(lái)管理 Zygote 進(jìn)程在啟動(dòng)過(guò)程中預(yù)加載和創(chuàng)建的各種對(duì)象,Zygote Space 中不會(huì)觸發(fā) GC,在 Zygote 進(jìn)程和應(yīng)用程序進(jìn)程之間會(huì)共享 Zygote Space。在 Zygote 進(jìn)程 fork 第一個(gè)子進(jìn)程之前會(huì)把 Zygote Space 分為兩部分,原來(lái)的已經(jīng)被使用的那部分堆仍稱為 Zygote Space,而未使用的那部分堆稱為 Allocation Space,以后的對(duì)象都會(huì)在 Allocation Space 上進(jìn)行分配和釋放。Allocation Space 不是進(jìn)程間共享的。除了這兩個(gè) Space,還包含以下數(shù)據(jù)結(jié)構(gòu):
- Card Table:用于 DVM Concurrent GC,當(dāng)?shù)谝淮芜M(jìn)行垃圾回收標(biāo)記后,記錄被標(biāo)記對(duì)象信息。
- Heap Bitmap:有兩個(gè) Heap Bitmap,一個(gè)用來(lái)記錄上次 GC 存活的對(duì)象,另一個(gè)用來(lái)記錄這次 GC 存活的對(duì)象。
- Mark Stack:DVM 的運(yùn)行時(shí)堆使用標(biāo)記——清除(Mark-Sweep)算法進(jìn)行 GC,Mark Stack 就是在 GC 的標(biāo)記階段使用的,用來(lái)遍歷存活的對(duì)象。
1.4 DVM 的 GC 日志
DVM 種的垃圾收集日志格式為:
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_status>, <Pause_time>
可以看出 DVM 的日志共有 5 個(gè)信息,其中 GC Reason 有多個(gè)。
1. 引起 GC 的原因
<GC_Reason> 就是引起 GC 的原因,有以下幾種:
- GC_CONCURRENT:當(dāng)堆開(kāi)始填充時(shí),并發(fā) GC 釋放內(nèi)存。
- GC_FOR_MALLOC:當(dāng)堆已滿時(shí),App 嘗試分配內(nèi)存而引起 GC,系統(tǒng)必須停止 App 并回收內(nèi)存。
- GC_HPROF_DUMP_HEAP:當(dāng)請(qǐng)求創(chuàng)建 HPROF 文件來(lái)分析堆內(nèi)存時(shí)出現(xiàn) GC。
- GC_EXPLICIT:顯式的 GC,例如調(diào)用 System.gc()(應(yīng)避免顯式的調(diào)用 GC,信任 GC 會(huì)在需要時(shí)運(yùn)行)。
- GC_EXTERNAL_ALLOC:僅適用于 API 級(jí)別小于等于 10,且用于外部分配內(nèi)存的 GC。
2. 其它的信息
除了引起 GC 的原因,其它信息如下:
- Amount_freed:本次 GC 釋放內(nèi)存的大小。
- Heap_stats:堆的空閑內(nèi)存百分比(已用內(nèi)存)/(堆的總內(nèi)存)。
- External_memory_stats:API 小于等于級(jí)別 10 的內(nèi)存分配(已分配的內(nèi)存)/(引起 GC 的閾值)。
- Pause_time:暫停時(shí)間,更大的堆會(huì)有更長(zhǎng)的暫停時(shí)間。并發(fā)暫停時(shí)間會(huì)顯示兩個(gè)暫停時(shí)間,一個(gè)出現(xiàn)在垃圾收集開(kāi)始時(shí),另一個(gè)出現(xiàn)在在垃圾收集快要完成時(shí)。
3. 實(shí)例分析:
D/dalvikvm: GC_CONCURRENT freed 2011K, 60% free 3200K/8900K, external 4501K/5421K, paused 2ms+2ms
含義如下:
引起 GC 的原因時(shí) GC_CONCURRENT;本次釋放的內(nèi)存為 2011KB;堆的空閑百分比為 60%,已使用內(nèi)存為 3200KB,堆的總內(nèi)存為 8900KB;暫停的總時(shí)長(zhǎng)為 4ms。
2. ART 虛擬機(jī)
ART(Android Runtime)虛擬機(jī)是 Android 4.4 發(fā)布的,用來(lái)替換 Dalvik 虛擬機(jī),Android 4.4 默認(rèn)采用 DVM,但是可以選擇使用 ART。在 Android 5.0 版本中默認(rèn)使用 ART,DVM 從此退出歷史舞臺(tái)。
2.1 ART 與 DVM 的區(qū)別
主要有以下 4 點(diǎn):
- DVM 中的應(yīng)用每次運(yùn)行時(shí),字節(jié)碼搜需要通過(guò) JIT 編譯器編譯成機(jī)器碼,這會(huì)使得應(yīng)用程序的運(yùn)行效率降低。而在 ART 中,系統(tǒng)在安裝應(yīng)用程序時(shí)會(huì)進(jìn)行一次 AOT(ahead of time compilation, 預(yù)編譯),將字節(jié)碼預(yù)先編譯成機(jī)器碼并存儲(chǔ)在本地,這樣應(yīng)用程序每次運(yùn)行時(shí)就不需要執(zhí)行編譯了,運(yùn)行 效率會(huì)大大提升,設(shè)備的耗電量也會(huì)降低。不過(guò)采用 AOT 也有缺點(diǎn),主要有兩個(gè):第一個(gè)是 AOT 會(huì)使得應(yīng)用程序的安裝時(shí)間變長(zhǎng),尤其是一些復(fù)雜的應(yīng)用;第二個(gè)是字節(jié)碼預(yù)先編譯成機(jī)器碼,機(jī)器碼需要的存儲(chǔ)空間會(huì)多一些。為了彌補(bǔ)以上兩個(gè)缺點(diǎn),Android 7.0 版本的 ART 加入了即時(shí)編譯器 JIT,作為 AOT 的一個(gè)補(bǔ)充,在應(yīng)用程序安裝時(shí)不會(huì)將字節(jié)碼全部編譯成機(jī)器碼,而是在運(yùn)行種將熱點(diǎn)代碼編譯成機(jī)器碼,從而縮短了應(yīng)用程序的安裝時(shí)間并節(jié)省了存儲(chǔ)空間。
- DVM 時(shí)為 32 位 CPU 設(shè)計(jì)的,而 ART 支持 64 位并兼容 32 位 CPU,這也是 DVM 被淘汰的主要原因之一。
- ART 對(duì)垃圾回收機(jī)制進(jìn)行了改進(jìn),比如更頻繁地執(zhí)行并行垃圾收集,將 GC 暫停由 2 次減少為 1 次等。
- ART 的運(yùn)行時(shí)堆空間劃分與 DVM 不同。
2.2 ART 的運(yùn)行時(shí)堆
與 DVM 的 GC 不同的是,ART 采用了多種垃圾回收方案,每個(gè)方案會(huì)運(yùn)行不同的垃圾收集器,默認(rèn)采用 CMS(Concurrent Mark-Sweep)方案,該方案主要使用了 sticky-CMS 和 partial-CMS。根據(jù)不同的 CMS 方案,ART 的運(yùn)行時(shí)堆的空間也有不同的劃分,默認(rèn)是由 4 個(gè) Space 和多個(gè)輔助數(shù)據(jù)結(jié)構(gòu)組成的,4 個(gè) Space 分別是 Zygote Space、Allocation Space、Image Space 和 Large Object Space。Zygote Space、Allocation Space 和 DVM 中的作用是一樣的,Image Space 用來(lái)存放一些預(yù)加載類,Large Object Space 用來(lái)分配一些大對(duì)象(默認(rèn)大小為 12KB),其中 Zygote Space 和 Image Space 是進(jìn)程間共享的。采用標(biāo)記——清除算法時(shí)的運(yùn)行時(shí)堆空間劃分如圖:
除了這四個(gè) Space,ART 的 Java 堆中還包括兩個(gè) Mod Union Table,一個(gè) Card Table,兩個(gè) Heap Bitmap,兩個(gè) Object Map,以及三個(gè) Object Stack。
2.3 ART 的 GC 日志
ART 的 GC 日志與 DVM 不同,ART 會(huì)為那些主動(dòng)請(qǐng)求的垃圾回收事件或者認(rèn)為 GC 速度慢時(shí)才會(huì)打印 GC 日志。GC 速度慢指的是 GC 暫停超過(guò) 5ms 或者 GC 持續(xù)時(shí)間超過(guò) 100ms.如果 App 未處于可察覺(jué)的暫停進(jìn)程狀態(tài),那么它的 GC 不會(huì)被認(rèn)為是慢速的。
ART 的 GC 日志具體格式為:
I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>
介紹如下:
1. 引起 GC 的原因
ART 的引起 GC 原因(GC_Reason)要比 DVM 多一些,有以下幾種:
- Concurrent:并發(fā) GC,不會(huì)使 App 的線程暫停,該 GC 是在后臺(tái)線程運(yùn)行的,并不會(huì)阻止內(nèi)存分配。
- Alloc:當(dāng)堆內(nèi)存已滿時(shí),App 嘗試分配內(nèi)存而引起的 GC,這個(gè) GC 會(huì)發(fā)生在正在分配內(nèi)存的線程中。
- Explicit:App 顯示的請(qǐng)求垃圾收集,例如調(diào)用 System.gc()。與 DVM 一樣,最佳做法是應(yīng)該信任 GC 并避免顯式地請(qǐng)求 GC,顯示地請(qǐng)求 GC 會(huì)阻止分配線程并不必要地浪費(fèi) CPU 周期。如果顯式地請(qǐng)求 GC 導(dǎo)致其他線程被搶占,那么有可能會(huì)導(dǎo)致 jank(App 同一幀畫(huà)了多次)。
- NativeAlloc:Native 內(nèi)存分配時(shí),比如為 Bitmaps 或者 RenderScript 分配對(duì)象,這會(huì)導(dǎo)致 Native 內(nèi)存壓力,從而觸發(fā) GC。
- CollectorTransition:由堆轉(zhuǎn)換引起的回收,這是運(yùn)行時(shí)切換 GC 而引起的。收集器轉(zhuǎn)換包括將所有對(duì)象從空閑列表空間復(fù)制到碰撞指針空間(反之亦然)。當(dāng)前,收集器轉(zhuǎn)換僅在以下情況出現(xiàn):在內(nèi)存較小的設(shè)備上,App 將進(jìn)程狀態(tài)從可察覺(jué)的暫停狀態(tài)變更為可察覺(jué)的非暫停狀態(tài)(反之亦然)。
- HomogeneousSpaceCompact:齊性空間壓縮,指空閑列表到壓縮的空閑列表空間,通常發(fā)生在當(dāng) App 已經(jīng)移動(dòng)到可察覺(jué)的暫停進(jìn)程狀態(tài)時(shí)。這樣做的主要原因是減少內(nèi)存使用并對(duì)堆內(nèi)存進(jìn)行碎片整理。
- DisableMovingGc:不是真正觸發(fā) GC 的原因,發(fā)生在并發(fā)堆壓縮時(shí),由于使用了 GetPrimitiveArrayCritical,收集會(huì)被阻塞。在一般情況下,強(qiáng)烈建議不要使用 GetPrimitiveArrayCritical,因?yàn)樗谝苿?dòng)收集器方面有限制。
- HeapTrim:不是觸發(fā) GC 的原因,但是需要注意,收集會(huì)一直被阻塞,直到堆內(nèi)存整理完畢。
2. 垃圾收集器名稱
GC_Name 指的是垃圾收集器名稱,有以下幾種:
- Concurrent Mark Sweep(CMS):CMS 收集器是一種以獲取最短收集暫停時(shí)間為目標(biāo)的收集器,采用標(biāo)記——清楚算法實(shí)現(xiàn)。它是完整的垃圾收集器,能釋放除了 Image Space 外的所有的空間。
- Concurrent Partial Mark Sweep:部分完整的垃圾收集器,能釋放除 Image Space 和 Zygote Space 外的所有空間。
- Concurrent Sticky Mark Sweep:粘性收集器,基于分帶垃圾收集思想,只能釋放上次 GC 以來(lái)分配的對(duì)象。這個(gè)垃圾收集器比一個(gè)完整或者部分完整的垃圾收集器掃描的更頻繁,因?yàn)樗於矣懈痰臅和r(shí)間。
- Marksweep + Semispace:非并發(fā)的 GC,復(fù)制 GC 用于堆轉(zhuǎn)換以及齊性空間壓縮(堆碎片整理)。
3. 其它信息
- Object freed:本次 GC 從非 Large Object Space 中回收的對(duì)象數(shù)量。
- Size_freed:本次 GC 從非 Large Object Space 中回收的字節(jié)數(shù)。
- Large objects freed:本次 GC 從 Large Object Space 中回收的對(duì)象數(shù)量。
- Large objects size freed:本次 GC 從 Large Object Space 中回收的字節(jié)數(shù)。
- Heap stats:堆的空閑內(nèi)存百分比,即(已用內(nèi)存)/(堆的總內(nèi)存)。
- Pause times:暫停時(shí)間,暫停時(shí)間與在 GC 運(yùn)行時(shí)修改的對(duì)象引用數(shù)量成比例。目前,ART 的 CMS 收集器僅有一次暫停,它出現(xiàn)在 GC 的結(jié)尾附近。有對(duì)象移動(dòng)的垃圾收集器暫停時(shí)間會(huì)很長(zhǎng),會(huì)在大部分垃圾回收期間連續(xù)出現(xiàn)。
4. 實(shí)例分析
I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.21ms
這個(gè) GC 日志的含義為引起 GC 原因是 Explicit; 垃圾收集器為 CMS 收集器;釋放對(duì)象的數(shù)量為 104710個(gè),釋放字節(jié)數(shù)為 7MB;釋放大對(duì)象的數(shù)量為 21 個(gè),釋放大對(duì)象字節(jié)數(shù)為 416KB;堆的空閑內(nèi)存百分比為 33%,已用內(nèi)存為 25MB,堆的總內(nèi)存為 38MB;GC 暫停時(shí)長(zhǎng)為 1.230ms,GC 總時(shí)長(zhǎng)為 67.21ms。