Java 虛擬機在前面的系列文章中有所介紹(http://www.reibang.com/c/84fea797b420)倾哺,Android 所使用的虛擬機(Dalvik 和 ART)與 JVM 有所不同衔肢,這篇文章來介紹一下。
1. Dalvik 虛擬機
Dalvik 虛擬機(Dalvik Virtual Machine)惹挟,簡稱 Dalvik VM 或者 DVM股淡。它是 Google 專門為 Android 平臺開發(fā)的虛擬機屯换,運行在 Android 運行時庫中乏奥。DVM 并不是一個 Java 虛擬機,原因在下面介紹登渣。
1.1 DVM 與 JVM 的區(qū)別
DVM 之所以不是一個 JVM噪服,主要原因是 DVM 并沒有遵循 JVM 規(guī)范來實現(xiàn),與 JVM 主要區(qū)別如下:
1. 基于的架構(gòu)不同
JVM 的執(zhí)行的指令是基于棧結(jié)構(gòu)胜茧,這就意味著需要去棧中讀寫數(shù)據(jù)粘优,所需的指令會很多,會導(dǎo)致速度變慢呻顽,對于性能有限的移動設(shè)備雹顺,顯然不合適。DVM 是基于寄存器的廊遍,沒有基于棧的虛擬機在復(fù)制數(shù)據(jù)時使用的大量的出入棧指令嬉愧,同時指令更緊湊、更簡介喉前。但是由于指定了操作數(shù)没酣,所以指令會比基于棧的指令大,但是由于指令數(shù)量的減少卵迂,總的代碼不會增加多少裕便。
2. 執(zhí)行的字節(jié)碼不同
在 Java SE 程序中,Java 類被編譯成一個或多個 .class 文件见咒,并打包成 jar 文件偿衰,而后 JVM 會通過相應(yīng)的 .class 文件和 jar 文件獲取對相應(yīng)的字節(jié)碼。執(zhí)行順序為:.java 文件 → .class 文件 → .jar 文件论颅,而 DVM 會用 dx 工具將所有的 .class 文件轉(zhuǎn)換為一個 .dex 文件哎垦,然后 DVM 會從該 .dex 文件讀取指令和數(shù)據(jù)囱嫩。執(zhí)行順序為:.java 文件 → .class 文件 → .dex 文件恃疯。
jar 文件和 apk 文件結(jié)構(gòu)如圖:
關(guān)于 class 文件結(jié)構(gòu)的詳細說明,可參考:
當 JVM 加載 .jar 文件的時候墨闲,會加載里面所有的 .class 文件今妄,這種加載方式對于性能有限的移動設(shè)備不合適。在 .apk 文件中鸳碧,一般情況下只包含一個 .dex 文件盾鳞,這個 .dex 文件把所有的 .class 文件信息整合在一起,這樣就提升了加載速度瞻离。.class 文件種也會存在一些冗余信息腾仅,dex 工具會去除冗余信息,并把所有的 .class 文件整合到 .dex 文件種套利,減少 I/O 操作推励,加快了類的查找速度鹤耍。
3. DVM 允許在有限的內(nèi)存中同時運行多個進程
DVM 經(jīng)過優(yōu)化,允許在有限的內(nèi)存中同時運行多個進程验辞。在 Android 中的每一個應(yīng)用都運行在一個 DVM 實例中稿黄,每一個 DVM 實例都運行在一個獨立的進程空間中,獨立的進程可以防止在虛擬機崩潰的時候所有的程序都關(guān)閉跌造。
4. DVM 由 Zygote 創(chuàng)建和初始化
Zygote 是第一個 DVM 進程杆怕,同時也用來創(chuàng)建和初始化 DVM 實例。每當系統(tǒng)需要創(chuàng)建一個應(yīng)用程序時壳贪,Zygote 就會 fork 自身陵珍,快速的創(chuàng)建和初始化一個 DVM 實例,用于應(yīng)用程序的運行违施。對于一些只讀的系統(tǒng)庫撑教,所有的 DVM 實例都會和 Zygote 共享一塊內(nèi)存區(qū)域,節(jié)省了內(nèi)存開銷醉拓。
5. DVM 有共享機制
DVM 擁有預(yù)加載——共享的機制伟姐,不同的應(yīng)用之間在運行時可以共享相同的類,擁有更高的效率亿卤。而 JVM 不存在這種共享機制愤兵,不同的程序,打包后彼此獨立排吴,即便它們使用了同樣的類秆乳,運行時也都是單獨加載和運行的,無法進行共享钻哩。
6. DVM 早期沒有使用 JIT 編譯器
早期的 DVM 沒有使用 JIT 編譯器屹堰,每次執(zhí)行代碼,都需要通過解釋器將 dex 代碼編譯成機器碼街氢,然后執(zhí)行扯键,效率不高。為了解決這一問題珊肃,從 Android 2.2 版本開始 DVM 使用了 JIT 編譯器荣刑,它會對多次運行的代碼(熱點代碼)進行編譯,生成精簡的本地機器碼(Native Code)伦乔,這樣在下次執(zhí)行到相同代碼時厉亏,可以直接使用機器碼執(zhí)行。但是烈和,應(yīng)用程序每次重新運行時爱只,都需要做 JIT 編譯工作。
1.2 DVM 架構(gòu)
DVM 源碼位于 dalvik/ 目錄下招刹,Android 8.0 及 9.0 中的 DVM 源碼部分目錄說明如下:
目錄/文件 | 說明 |
---|---|
dexdump | 生成 dex 文件的反編譯查看工具恬试,主要用來查看編譯出來的代碼的正確性和結(jié)構(gòu) |
dexgen | dex 代碼生成器項目 |
docs | DVM 相關(guān)幫助文檔 |
dx | Java 字節(jié)碼轉(zhuǎn)換成 DVM 機器碼的工具 |
libdex | 生成主機和設(shè)備處理 dex 文件的庫 |
tools | 一些編譯和運行相關(guān)的工具 |
Android.mk | 虛擬機編譯的 makefile 配置文件 |
MODULE_LICENSE_APACHE2 | APACHE2 版權(quán)聲明文件 |
NOTICE | 虛擬機源碼版權(quán)注意事項文件 |
其中沥匈,dalvik/libdex 會被編譯成 libdex.a 靜態(tài)庫,作為 dex 工具使用忘渔;dalvik/dexdump 是 .dex 文件的反編譯工具高帖,DVM 架構(gòu)如圖:
首先 Java 編譯器編譯的 .class 文件經(jīng)過 dx 工具轉(zhuǎn)換為 .dex 文件,.dex 文件由類加載器處理畦粮,接著解釋器根據(jù)指令集對 Dalvik 字節(jié)碼進行解釋散址、執(zhí)行,最后交由 Linux 處理宣赔。
1.3 DVM 的運行時堆
DVM 的運行時堆使用標記——清除(Mark-Sweep)算法進行 GC预麸,它由兩個 Space 以及多個輔助數(shù)據(jù)結(jié)構(gòu)組成,兩個 Space 分別是 Zygote Space(Zygote Heap)和 Allocation Space(Active Heap)儒将。Zygote Space 用來管理 Zygote 進程在啟動過程中預(yù)加載和創(chuàng)建的各種對象吏祸,Zygote Space 中不會觸發(fā) GC,在 Zygote 進程和應(yīng)用程序進程之間會共享 Zygote Space砰逻。在 Zygote 進程 fork 第一個子進程之前會把 Zygote Space 分為兩部分蝠咆,原來的已經(jīng)被使用的那部分堆仍稱為 Zygote Space,而未使用的那部分堆稱為 Allocation Space闸翅,以后的對象都會在 Allocation Space 上進行分配和釋放坚冀。Allocation Space 不是進程間共享的遗菠。除了這兩個 Space华蜒,還包含以下數(shù)據(jù)結(jié)構(gòu):
- Card Table:用于 DVM Concurrent GC叭喜,當?shù)谝淮芜M行垃圾回收標記后捂蕴,記錄被標記對象信息。
- Heap Bitmap:有兩個 Heap Bitmap涡匀,一個用來記錄上次 GC 存活的對象陨瘩,另一個用來記錄這次 GC 存活的對象舌劳。
- Mark Stack:DVM 的運行時堆使用標記——清除(Mark-Sweep)算法進行 GC甚淡,Mark Stack 就是在 GC 的標記階段使用的贯卦,用來遍歷存活的對象脸侥。
1.4 DVM 的 GC 日志
DVM 種的垃圾收集日志格式為:
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_status>, <Pause_time>
可以看出 DVM 的日志共有 5 個信息睁枕,其中 GC Reason 有多個沸手。
1. 引起 GC 的原因
<GC_Reason> 就是引起 GC 的原因契吉,有以下幾種:
- GC_CONCURRENT:當堆開始填充時菲语,并發(fā) GC 釋放內(nèi)存送巡。
- GC_FOR_MALLOC:當堆已滿時英支,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()(應(yīng)避免顯式的調(diào)用 GC,信任 GC 會在需要時運行)蚣录。
- GC_EXTERNAL_ALLOC:僅適用于 API 級別小于等于 10萎河,且用于外部分配內(nèi)存的 GC虐杯。
2. 其它的信息
除了引起 GC 的原因擎椰,其它信息如下:
- Amount_freed:本次 GC 釋放內(nèi)存的大小达舒。
- Heap_stats:堆的空閑內(nèi)存百分比(已用內(nèi)存)/(堆的總內(nèi)存)巩搏。
- External_memory_stats:API 小于等于級別 10 的內(nèi)存分配(已分配的內(nèi)存)/(引起 GC 的閾值)贯底。
- Pause_time:暫停時間禽捆,更大的堆會有更長的暫停時間胚想。并發(fā)暫停時間會顯示兩個暫停時間顿仇,一個出現(xiàn)在垃圾收集開始時臼闻,另一個出現(xiàn)在在垃圾收集快要完成時述呐。
3. 實例分析:
D/dalvikvm: GC_CONCURRENT freed 2011K, 60% free 3200K/8900K, external 4501K/5421K, paused 2ms+2ms
含義如下:
引起 GC 的原因時 GC_CONCURRENT;本次釋放的內(nèi)存為 2011KB代虾;堆的空閑百分比為 60%棉磨,已使用內(nèi)存為 3200KB乘瓤,堆的總內(nèi)存為 8900KB抬吟;暫停的總時長為 4ms。
2. ART 虛擬機
ART(Android Runtime)虛擬機是 Android 4.4 發(fā)布的,用來替換 Dalvik 虛擬機聪建,Android 4.4 默認采用 DVM刃鳄,但是可以選擇使用 ART叔锐。在 Android 5.0 版本中默認使用 ART愉烙,DVM 從此退出歷史舞臺步责。
2.1 ART 與 DVM 的區(qū)別
主要有以下 4 點:
- DVM 中的應(yīng)用每次運行時遂鹊,字節(jié)碼搜需要通過 JIT 編譯器編譯成機器碼秉扑,這會使得應(yīng)用程序的運行效率降低调限。而在 ART 中秦躯,系統(tǒng)在安裝應(yīng)用程序時會進行一次 AOT(ahead of time compilation, 預(yù)編譯),將字節(jié)碼預(yù)先編譯成機器碼并存儲在本地裆装,這樣應(yīng)用程序每次運行時就不需要執(zhí)行編譯了踱承,運行 效率會大大提升,設(shè)備的耗電量也會降低米母。不過采用 AOT 也有缺點勾扭,主要有兩個:第一個是 AOT 會使得應(yīng)用程序的安裝時間變長,尤其是一些復(fù)雜的應(yīng)用铁瞒;第二個是字節(jié)碼預(yù)先編譯成機器碼妙色,機器碼需要的存儲空間會多一些。為了彌補以上兩個缺點慧耍,Android 7.0 版本的 ART 加入了即時編譯器 JIT身辨,作為 AOT 的一個補充芍碧,在應(yīng)用程序安裝時不會將字節(jié)碼全部編譯成機器碼踪危,而是在運行種將熱點代碼編譯成機器碼,從而縮短了應(yīng)用程序的安裝時間并節(jié)省了存儲空間。
- DVM 時為 32 位 CPU 設(shè)計的,而 ART 支持 64 位并兼容 32 位 CPU棚饵,這也是 DVM 被淘汰的主要原因之一。
- ART 對垃圾回收機制進行了改進诈胜,比如更頻繁地執(zhí)行并行垃圾收集缓熟,將 GC 暫停由 2 次減少為 1 次等彰触。
- ART 的運行時堆空間劃分與 DVM 不同俭茧。
2.2 ART 的運行時堆
與 DVM 的 GC 不同的是,ART 采用了多種垃圾回收方案,每個方案會運行不同的垃圾收集器,默認采用 CMS(Concurrent Mark-Sweep)方案,該方案主要使用了 sticky-CMS 和 partial-CMS者铜。根據(jù)不同的 CMS 方案衣厘,ART 的運行時堆的空間也有不同的劃分,默認是由 4 個 Space 和多個輔助數(shù)據(jù)結(jié)構(gòu)組成的,4 個 Space 分別是 Zygote Space潭千、Allocation Space路翻、Image Space 和 Large Object Space脐雪。Zygote Space获询、Allocation Space 和 DVM 中的作用是一樣的尝哆,Image Space 用來存放一些預(yù)加載類恒序,Large Object Space 用來分配一些大對象(默認大小為 12KB),其中 Zygote Space 和 Image Space 是進程間共享的厉碟。采用標記——清除算法時的運行時堆空間劃分如圖:
除了這四個 Space逢并,ART 的 Java 堆中還包括兩個 Mod Union Table玻蝌,一個 Card Table瓦糟,兩個 Heap Bitmap考余,兩個 Object Map先嬉,以及三個 Object Stack。
2.3 ART 的 GC 日志
ART 的 GC 日志與 DVM 不同秃殉,ART 會為那些主動請求的垃圾回收事件或者認為 GC 速度慢時才會打印 GC 日志坝初。GC 速度慢指的是 GC 暫停超過 5ms 或者 GC 持續(xù)時間超過 100ms.如果 App 未處于可察覺的暫停進程狀態(tài),那么它的 GC 不會被認為是慢速的钾军。
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,不會使 App 的線程暫停吏恭,該 GC 是在后臺線程運行的拗小,并不會阻止內(nèi)存分配。
- Alloc:當堆內(nèi)存已滿時樱哼,App 嘗試分配內(nèi)存而引起的 GC哀九,這個 GC 會發(fā)生在正在分配內(nèi)存的線程中剿配。
- Explicit:App 顯示的請求垃圾收集,例如調(diào)用 System.gc()阅束。與 DVM 一樣呼胚,最佳做法是應(yīng)該信任 GC 并避免顯式地請求 GC,顯示地請求 GC 會阻止分配線程并不必要地浪費 CPU 周期息裸。如果顯式地請求 GC 導(dǎo)致其他線程被搶占蝇更,那么有可能會導(dǎo)致 jank(App 同一幀畫了多次)。
- NativeAlloc:Native 內(nèi)存分配時呼盆,比如為 Bitmaps 或者 RenderScript 分配對象年扩,這會導(dǎo)致 Native 內(nèi)存壓力,從而觸發(fā) GC访圃。
- CollectorTransition:由堆轉(zhuǎn)換引起的回收厨幻,這是運行時切換 GC 而引起的。收集器轉(zhuǎn)換包括將所有對象從空閑列表空間復(fù)制到碰撞指針空間(反之亦然)腿时。當前况脆,收集器轉(zhuǎn)換僅在以下情況出現(xiàn):在內(nèi)存較小的設(shè)備上,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,因為它在移動收集器方面有限制宵荒。
- HeapTrim:不是觸發(fā) GC 的原因汁雷,但是需要注意,收集會一直被阻塞报咳,直到堆內(nèi)存整理完畢侠讯。
2. 垃圾收集器名稱
GC_Name 指的是垃圾收集器名稱,有以下幾種:
- Concurrent Mark Sweep(CMS):CMS 收集器是一種以獲取最短收集暫停時間為目標的收集器暑刃,采用標記——清楚算法實現(xiàn)厢漩。它是完整的垃圾收集器,能釋放除了 Image Space 外的所有的空間岩臣。
- Concurrent Partial Mark Sweep:部分完整的垃圾收集器溜嗜,能釋放除 Image Space 和 Zygote Space 外的所有空間宵膨。
- Concurrent Sticky Mark Sweep:粘性收集器,基于分帶垃圾收集思想炸宵,只能釋放上次 GC 以來分配的對象辟躏。這個垃圾收集器比一個完整或者部分完整的垃圾收集器掃描的更頻繁,因為它更快而且有更短的暫停時間土全。
- Marksweep + Semispace:非并發(fā)的 GC鸿脓,復(fù)制 GC 用于堆轉(zhuǎn)換以及齊性空間壓縮(堆碎片整理)。
3. 其它信息
- Object freed:本次 GC 從非 Large Object Space 中回收的對象數(shù)量涯曲。
- Size_freed:本次 GC 從非 Large Object Space 中回收的字節(jié)數(shù)野哭。
- Large objects freed:本次 GC 從 Large Object Space 中回收的對象數(shù)量。
- Large objects size freed:本次 GC 從 Large Object Space 中回收的字節(jié)數(shù)幻件。
- Heap stats:堆的空閑內(nèi)存百分比拨黔,即(已用內(nèi)存)/(堆的總內(nèi)存)。
- Pause times:暫停時間绰沥,暫停時間與在 GC 運行時修改的對象引用數(shù)量成比例篱蝇。目前,ART 的 CMS 收集器僅有一次暫停徽曲,它出現(xiàn)在 GC 的結(jié)尾附近零截。有對象移動的垃圾收集器暫停時間會很長,會在大部分垃圾回收期間連續(xù)出現(xiàn)秃臣。
4. 實例分析
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
這個 GC 日志的含義為引起 GC 原因是 Explicit涧衙; 垃圾收集器為 CMS 收集器;釋放對象的數(shù)量為 104710個奥此,釋放字節(jié)數(shù)為 7MB弧哎;釋放大對象的數(shù)量為 21 個,釋放大對象字節(jié)數(shù)為 416KB稚虎;堆的空閑內(nèi)存百分比為 33%撤嫩,已用內(nèi)存為 25MB,堆的總內(nèi)存為 38MB蠢终;GC 暫停時長為 1.230ms序攘,GC 總時長為 67.21ms。