目錄
1. Dalvik
1.1 Dalvik 和 JVM 區別
1.2 Dalvik 如何運行 java
1.3 dex檔案
2. Android Runtime (ART)
2.1 ART 功能
2.1.1 預先 (AOT) 編譯
2.1.2 垃圾回收方面的優化
2.1.3 開發和除錯方面的優化
2.2 Android 8.0 中的 ART 功能改進
2.2.1 并發壓縮式垃圾回收器
2.2.2 回圈優化
2.2.3 類層次結構分析
2.2.4 .oat 檔案中的內嵌快取
2.2.5 Dexlayout
2.2.6 Dex 快取移除
2.2.7 解釋器性能
2.2.8 詳細了解內嵌
2.2.9 同步方面的改進
2.2.10 更快速的原生方法
3. 記憶體管理
3.1 ART GC 概覽
3.2 垃圾回收
3.3 共享記憶體
3.4 分配與回收應用記憶體
3.5 限制應用記憶體
3.6 切換應用
1. Dalvik
Dalvik是Google公司自己設計用于Android平臺的虛擬機,它可以支持已轉換為 .dex(即Dalvik Executable)格式的Java應用程式的運行,.dex 格式是專為Dalvik設計的一種壓縮格式,適合記憶體和處理器速度有限的系統,Dalvik 經過優化,允許在有限的記憶體中同時運行多個虛擬機的實體,并且每一個Dalvik 應用作為一個獨立的Linux 行程執行,獨立的行程可以防止在虛擬機崩潰的時候所有程式都被關閉,
在 Android L (Android 5.0) 之前叫作 DVM,5.0 之后直接洗掉DVM,代替它的是傳聞已久的ART(Android Runtime),
在整個 Android 作業系統體系中,ART 位于下圖黃色小方塊位置:

不是說被洗掉就無用了,咱們畢竟做這一行的還是要簡單的了解一下,
1.1 Dalvik 和 JVM 區別
-
Dalvik 基于暫存器,而 JVM 基于堆疊,
-
基于暫存器的虛擬機對于更大的程式來講,在它們編譯的時候,花費的時間更短,
-
JVM位元組碼中,區域變數會被放入區域變數表中,繼而被壓入堆疊供操做碼進行運算,固然JVM也能夠只使用堆疊而不顯式地將區域變數存入變數表中,
-
Dalvik位元組碼中,區域變數會被賦給65536個可用的暫存器中的任何一個,Dalvik指令直接操做這些暫存器,而不是訪問堆疊中的元素,
1.2 Dalvik 如何運行 java
-
VM位元組碼由.class檔案組成,每一個檔案一個class,
-
JVM在運行的時候為每個類裝載位元組碼,相反的,Dalvik程式只包含一個.dex檔案,這個檔案包含了程式中全部的類,
-
Java編譯器建立了JVM位元組碼以后,Dalvik的dx編譯器洗掉.class檔案,從新把它們編譯成Dalvik位元組碼,而后把它們寫進一個.dex檔案中,這個程序包括翻譯、重構、解釋程式的基本元素(常量池、類定義、資料段),
-
常量池描述了全部的常量,包括參考、方法名、數值常量等,類定義包括了訪問標志、類名等基本資訊,資料段中包含各類被VM執行的函式代碼以及類和函式的相關資訊(例如DVM所須要的暫存器數量、區域變數表、操做數堆疊大小),還有實體變數,
1.3 dex檔案
class 檔案是由一個 java 原始碼檔案生成的 class 檔案,而 Android 是把所有 class 檔案進行合并優化,然后生成一個最終的 class.dex 檔案,dex 檔案去除了 class 檔案中的冗余資訊(比如重復字符常量),并且結構更加緊湊,因此在 dex 決議階段,可以減少 I/O 操作,提高了類的查找速度,
實際上,dex 檔案在 App 安裝程序中還會被進一步優化為 odex(optimized dex),此程序還會在后續介紹安裝程序時再次提到,
注意:這一優化程序也會伴隨著一些副作用,最經典的就是 Android 65535 問題,
65535 代表 dex 檔案中的方法個數、屬性個數、以及類的個數,也就是說理論上不止方法數,我們在 java 檔案中宣告的變數,或者創建的類個數如果也超過 65535 個,同樣會編譯失敗,Android 提供了 MultiDex 來解決這個問題,很多網上的文章說 65535 問題是因為決議 dex 檔案到資料結構 DexFile 時,使用了 short 來存盤方法的個數,其實這種說法是錯誤的!
Android 65535問題解決
2. Android Runtime (ART)
Android Runtime (ART) 是運行 Android 5.0(API 級別 21)及更高版本的設備的默認運行時,此運行時提供大量功能,可提升 Android 平臺和應用的性能和流暢度,
ART 是 Android 上的應用和部分系統服務使用的托管式運行時,ART 及其前身 Dalvik 最初是專為 Android 專案打造的,作為運行時的 ART 可執行 Dalvik 可執行檔案并遵循 Dex 位元組碼規范,
ART 和 Dalvik 是運行 Dex 位元組碼的兼容運行時,因此針對 Dalvik 開發的應用也能在 ART 環境中運作,不過,Dalvik 采用的一些技術并不適用于 ART,
2.1 ART 功能
2.1.1 預先 (AOT) 編譯
ART 引入了預先編譯機制,可提高應用的性能,ART 還具有比 Dalvik 更嚴格的安裝時驗證,
在安裝時,ART 使用設備自帶的 dex2oat 工具來編譯應用,此實用工具接受 DEX 檔案作為輸入,并為目標設備生成經過編譯的應用可執行檔案,該工具應能夠順利編譯所有有效的 DEX 檔案,但是,一些后處理工具會生成無效檔案,Dalvik 可以接受這些檔案,但 ART 無法編譯這些檔案,
2.1.2 垃圾回收方面的優化
垃圾回收 (GC) 會耗費大量資源,這可能有損于應用性能,導致顯示不穩定、界面回應速度緩慢以及其他問題,ART 通過以下幾種方式對垃圾回收做了優化:
-
大多采用并發設計,具有一次 GC 暫停;
-
并發復制,可減少后臺記憶體使用和碎片;
-
GC 暫停的時間不受堆大小影響;
-
在清理最近分配的短時物件這種特殊情況中,回收器的總 GC 時間更短;
-
優化了垃圾回收的工效,能夠更加及時地進行并行垃圾回收,這使得 GC_FOR_ALLOC 事件在典型用例中極為罕見,
2.1.3 開發和除錯方面的優化
ART 提供了大量功能來優化應用開發和除錯,
2.1.3.1 支持采樣分析器
一直以來,開發者都使用 Traceview 工具(用于跟蹤應用執行情況)作為分析器,雖然 Traceview 可提供有用的資訊,但每次方法呼叫產生的開銷會導致 Dalvik 分析結果出現偏差,而且使用該工具明顯會影響運行時性能,
ART 添加了對沒有這些限制的專用采樣分析器的支持,因而可更準確地了解應用執行情況,而不會明顯減慢速度,KitKat 版本為 Dalvik 的 Traceview 添加了采樣支持,
Traceview:Traceview 已棄用,如果您使用的是 Android Studio 3.2 或更高版本,應改為使用 CPU 性能剖析器 來執行以下操作:檢查通過使用 Debug 類檢測應用而捕獲的 .trace 檔案,記錄新方法跟蹤記錄,保存 .trace 檔案,以及檢查應用行程的實時 CPU 使用情況,
2.1.3.2 支持更多除錯功能
ART 支持許多新的除錯選項,特別是與監控和垃圾回收相關的功能,例如,您可以:
-
查看堆疊跟蹤中保留了哪些鎖,然后跳轉到持有鎖的執行緒,
-
詢問指定類的當前活動的實體數、請求查看實體,以及查看使物件保持有效狀態的參考,
-
過濾特定實體的事件(如斷點),
-
查看方法退出(使用“method-exit”事件)時回傳的值,
-
設定欄位觀察點,以在訪問和/或修改特定欄位時暫停程式執行,
2.1.3.3 優化了例外和崩潰報告中的診斷詳細資訊
當發生運行時例外時,ART 會為您提供盡可能多的背景關系和詳細資訊,ART 會提供 java.lang.ClassCastException、java.lang.ClassNotFoundException 和 java.lang.NullPointerException 的更多例外詳細資訊,(較高版本的 Dalvik 會提供 java.lang.ArrayIndexOutOfBoundsException 和 java.lang.ArrayStoreException 的更多例外詳細資訊,這些資訊現在包括陣列大小和越界偏移量;ART 也提供這類資訊,)
ART 還通過納入 Java 和原生堆疊資訊,在應用原生代碼崩潰報告中提供更實用的背景關系資訊,
2.2 Android 8.0 中的 ART 功能改進
在 Android 8.0 版本中,Android Runtime (ART) 有了極大改進,下面的串列總結了設備制造商可以在 ART 中獲得的增強功能,
2.2.1 并發壓縮式垃圾回收器
正如 Google 在 Google I/O 大會上所宣布的那樣,ART 在 Android 8.0 中提供了新的并發壓縮式垃圾回收器 (GC),該回收器會在每次執行 GC 時以及應用正在運行時對堆進行壓縮,且僅在處理執行緒根時短暫停頓一次,該回收器具有以下優勢:
-
GC 始侄訓對堆進行壓縮:堆的大小平均比 Android 7.0 中的小 32%,
-
得益于壓縮,系統現可實作執行緒區域碰撞指標物件分配:分配速度比 Android 7.0 中的快 70%,
-
H2 基準的停頓次數比 Android 7.0 GC 的少 85%,
-
停頓次數不再隨堆的大小而變化,應用在使用較大的堆時也無需擔心造成卡頓,
- GC 實作細節 - 讀取屏障:
-
讀取屏障是在讀取每個物件欄位時所做的少量作業,
-
它們在編譯器中經過了優化,但可能會減慢某些用例的速度,
-
2.2.2 回圈優化
在 Android 8.0 版本中,ART 采取了多種回圈優化措施,具體如下:
- 消除邊界檢查
-
靜態:在編譯時證明范圍位于邊界內
-
動態:運行時測驗確保回圈始終位于邊界內(否則不進行優化)
-
- 消除歸納變數
-
移除無用歸納
-
用封閉式運算式替換僅在回圈后使用的歸納
-
-
消除回圈主體內的無用代碼,移除整個死回圈
-
強度降低
-
回圈轉換:逆轉、交換、拆分、展開、單模等
-
SIMDization(也稱為矢量化)
回圈優化器位于 ART 編譯器中一個獨立的優化環節中,大多數回圈優化與其他方面的優化和簡化類似,采用比平時更復雜的方式進行一些重寫 CFG 的優化時會面臨挑戰,因為大多數 CFG 實用工具(請參閱 nodes.h)都側重于構建而不是重寫 CFG,
2.2.3 類層次結構分析
在 Android 8.0 中,ART 會使用類層次結構分析 (CHA),這是一種編譯器優化,可根據對類層次結構的分析結果,將虛擬呼叫去虛擬化為直接呼叫,虛擬呼叫代價高昂,因為它們圍繞 vtable 查找來實作,且會占用幾個依賴負載,另外,虛擬呼叫也不能內嵌,
以下是對相關增強功能的總結:
-
動態單一實作方法狀態更新 - 在類關聯時間結束時,如果 vtable 已被填充,ART 會按條目對超類的 vtable 進行比較,
-
編譯器優化 - 編譯器會利用某種方法的單一實作資訊,如果方法 A.foo 設定了單一實作標記,則編譯器會將虛擬呼叫去虛擬化為直接呼叫,并借此進一步嘗試內嵌直接呼叫,
-
已編譯代碼無效 - 另外,在類關聯時間結束時,如果單一實作資訊已更新,且方法 A.foo 之前擁有單一實作,但該狀態現已變為無效,則依賴方法 A.foo 擁有單一實作這一假設的所有已編譯代碼都需要變為無效代碼,
-
去優化 - 對于堆疊上已編譯的有效代碼,系統會啟動去優化功能,以強制使已編譯無效代碼進入解釋器模式,從而確保正確性,系統會采用結合了同步和異步去優化的全新去優化機制,
2.2.4 .oat 檔案中的內嵌快取
ART 現在采用內嵌快取,并對有足夠資料可用的呼叫站點進行優化,內嵌快取功能會將額外的運行時資訊記錄到組態檔中,并利用這類資訊將動態優化添加到預先編譯中,
2.2.5 Dexlayout
Dexlayout 是在 Android 8.0 中引入的一個庫,用于分析 dex 檔案,并根據組態檔對其進行重新排序,Dexlayout 旨在使用運行時配置資訊,在設備的空閑維護編譯期間對 dex 檔案的各個部分進行重新排序,通過將經常一起訪問的部分 dex 檔案集中在一起,程式可以因改進檔案位置而擁有更好的記憶體訪問模式,從而節省 RAM 并縮短啟動時間,
由于組態檔資訊目前僅在運行應用后可用,因此系統會在空閑維護期間將 dexlayout 集成到 dex2oat 的設備編譯中,
2.2.6 Dex 快取移除
在 Android 7.0 及更低版本中,DexCache 物件擁有四個大型陣列,與 DexFile 中特定元素的數量成正比,即:
-
字串(每個 DexFile::StringId 一個參考),
-
型別(每個 DexFile::TypeId 一個參考),
-
方法(每個 DexFile::MethodId 一個原生指標),
-
欄位(每個 DexFile::FieldId 一個原生指標),
這些陣列用于快速檢索我們以前決議的物件,在 Android 8.0 中,除方法陣列外,所有陣列都已移除,
2.2.7 解釋器性能
在 Android 7.0 版本中,通過引入 mterp(一種解釋器,具有以匯編語言撰寫的核心提取/解碼/解釋機制),解釋器性能得以顯著提升,Mterp 模仿了快速 Dalvik 解釋器,并支持 arm、arm64、x86、x86_64、mips 和 mips64,對于計算代碼而言,ART 的 Mterp 大致相當于 Dalvik 的快速解釋器,不過,有時候,它的速度可能會顯著變慢,甚至急劇變慢:
-
呼叫性能,
-
字串操作和 Dalvik 中其他被視為內嵌函式的高頻用戶方法,
-
堆疊記憶體使用量較高,
Android 8.0 解決了這些問題,
2.2.8 詳細了解內嵌
從 Android 6.0 開始,ART 可以內嵌同一個 dex 檔案中的任何呼叫,但只能內嵌來自其他 dex 檔案的葉方法,此項限制具有以下兩個原因:
-
從其他 dex 檔案進行內嵌要求使用該 dex 檔案的 dex 快取,這與同一個 dex 檔案內嵌(只需重復使用呼叫方的 dex 快取)有所不同,已編譯代碼中需要具有 dex 快取,以便執行一系列指令,例如靜態呼叫、字串加載或類加載,
-
堆疊映射只對當前 dex 檔案中的方法索引進行編碼,
為了應對這些限制,Android 8.0 做出了以下改進:
-
從已編譯代碼中移除 dex 快取訪問(另請參閱“Dex 快取移除”部分)
-
擴展堆疊映射編碼,
2.2.9 同步方面的改進
ART 團隊調整了 MonitorEnter/MonitorExit 代碼路徑,并減少了我們對 ARMv8 上傳統記憶體屏障的依賴,盡可能將其替換為較新的(獲取/釋放)指令,
2.2.10 更快速的原生方法
使用 @FastNative 和 @CriticalNative 注解可以更快速地對 Java 原生介面 (JNI) 進行原生呼叫,這些內置的 ART 運行時優化可以加快 JNI 轉換速度,并取代了現已棄用的 !bang JNI 標記,這些注解對非原生方法沒有任何影響,并且僅適用于 bootclasspath 上的平臺 Java 語言代碼(無 Play 商店更新),
@FastNative 注解支持非靜態方法,如果某種方法將 jobject 作為引數或回傳值進行訪問,請使用此注解,
利用 @CriticalNative 注解,可更快速地運行原生方法,但存在以下限制:
-
方法必須是靜態方法 - 沒有引數、回傳值或隱式 this 的物件,
-
僅將基元型別傳遞給原生方法,
-
原生方法在其函式定義中不使用 JNIEnv 和 jclass 引數,
-
方法必須使用 RegisterNatives 進行注冊,而不是依靠動態 JNI 鏈接,
@FastNative 和 @CriticalNative 注解在執行原生方法時會停用垃圾回收,不要與長時間運行的方法一起使用,包括通常很快但一般不受限制的方法,
停頓垃圾回收可能會導致死鎖,如果鎖尚未得到本地釋放(即尚未回傳受管理代碼),請勿在原生快速呼叫期間獲取鎖,此要求不適用于常規的 JNI 呼叫,因為 ART 將正執行的原生代碼視為已暫停的狀態,
@FastNative 可以使原生方法的性能提升高達 3 倍,而 @CriticalNative 可以使原生方法的性能提升高達 5 倍,
更多內容請參考:Android Runtime (ART) 和 Dalvik
3. 記憶體管理
Android Runtime (ART) 和 Dalvik 虛擬機使用分頁和記憶體映射來管理記憶體,這意味著應用修改的任何記憶體,無論修改的方式是分配新物件還是輕觸記憶體映射的頁面,都會一直駐留在 RAM 中,并且無法換出,要從應用中釋放記憶體,只能釋放應用保留的物件參考,使記憶體可供垃圾回收器回收,這種情況有一個例外:對于任何未經修改的記憶體映射檔案(如代碼),如果系統想要在其他位置使用其記憶體,可將其從 RAM 中換出,
3.1 ART GC 概覽
ART 有多個不同的 GC 方案,涉及運行不同的垃圾回收器,從 Android 8 (Oreo) 開始,默認方案是并發復制 (CC),另一個 GC 方案是并發標記清除 (CMS),
并發復制 GC 的一些主要特性包括:
-
CC 支持使用名為“RegionTLAB”的觸碰指標分配器,此分配器可以向每個應用執行緒分配一個執行緒本地分配緩沖區 (TLAB),這樣,應用執行緒只需觸碰“堆疊頂”指標,而無需任何同步操作,即可從其 TLAB 中將物件分配出去,
-
CC 通過在不暫停應用執行緒的情況下并發復制物件來執行堆碎片整理,這是在讀取屏障的幫助下實作的,讀取屏障會攔截來自堆的參考讀取,無需應用開發者進行任何干預,
-
GC 只有一次很短的暫停,對于堆大小而言,該次暫停在時間上是一個常量,
-
在 Android 10 及更高版本中,CC 會擴展為分代 GC,它支持輕松回收存留期較短的物件,這類物件通常很快便會無法訪問,這有助于提高 GC 吞吐量,并顯著延遲執行全堆 GC 的需要,
ART 仍然支持的另一個 GC 方案是 CMS,此 GC 方案還支持壓縮,但不是以并發方式,在應用進入后臺之前,它會避免執行壓縮,應用進入后臺后,它會暫停應用執行緒以執行壓縮,如果物件分配因碎片而失敗,也必須執行壓縮操作,在這種情況下,應用可能會在一段時間內沒有回應,
由于 CMS 很少進行壓縮,因此空閑物件可能會不連續,CMS 使用一個名為 RosAlloc 的基于空閑串列的分配器,與 RegionTLAB 相比,該分配器的分配成本較高,最后,由于內部碎片,Java 堆的 CMS 記憶體用量可能會高于 CC 記憶體用量,
3.2 垃圾回收
ART 或 Dalvik 虛擬機之類的受管記憶體環境會跟蹤每次記憶體分配,一旦確定程式不再使用某塊記憶體,它就會將該記憶體重新釋放到堆中,無需程式員進行任何干預,這種回收受管記憶體環境中的未使用記憶體的機制稱為"垃圾回收",垃圾回收有兩個目標:
-
在程式中查找將來無法訪問的數物件
-
回收這些物件使用的資源,
Android 的記憶體堆是分代的,這意味著它會根據分配物件的預期壽命和大小跟蹤不同的分配存盤磁區,
可參閱: Java 垃圾回收(GC)
3.3 共享記憶體
為了在 RAM 中容納所需的一切,Android 會嘗試跨行程共享 RAM 頁面,它可以通過以下方式實作這一點:
-
每個應用行程都從一個名為 Zygote 的現有行程分叉,系統啟動并加載通用框架代碼和資源(如 Activity 主題背景)時,Zygote 行程隨之啟動,為啟動新的應用行程,系統會分叉 Zygote 行程,然后在新行程中加載并運行應用代碼,這種方法使為框架代碼和資源分配的大多數 RAM 頁面可在所有應用行程之間共享,
-
大多數靜態資料會記憶體映射到一個行程中,這種方法使得資料不僅可以在行程之間共享,還可以在需要時換出,靜態資料示例包括:Dalvik 代碼(通過將其放入預先鏈接的 .odex 檔案中進行直接記憶體映射)、應用資源(通過將資源表格設計為可記憶體映射的結構以及通過對齊 APK 的 zip 條目)和傳統專案元素(如 .so 檔案中的原生代碼),
-
在很多地方,Android 使用明確分配的共享記憶體區域(通過 ashmem 或 gralloc)在行程間共享同一動態 RAM,例如,視窗 surface 使用在應用和螢屏合成器之間共享的記憶體,而游標緩沖區則使用在內容提供器和客戶端之間共享的記憶體,
由于共享記憶體的廣泛使用,在確定應用使用的記憶體量時需要小心謹慎,
3.4 分配與回收應用記憶體
Dalvik 堆局限于每個應用行程的單個虛擬記憶體范圍,這定義了邏輯堆大小,該大小可以根據需要增長,但不能超過系統為每個應用定義的上限,
堆的邏輯大小與堆使用的物理記憶體量不同,在檢查應用堆時,Android 會計算按比例分攤的記憶體大小 (PSS) 值,該值同時考慮與其他行程共享的臟頁和干凈頁,但其數量與共享該 RAM 的應用數量成正比,此 (PSS) 總量是系統認為的物理記憶體占用量,
Dalvik 堆不壓縮堆的邏輯大小,這意味著 Android 不會對堆進行碎片整理來縮減空間,只有當堆末尾存在未使用的空間時,Android 才能縮減邏輯堆大小,但是,系統仍然可以減少堆使用的物理記憶體,垃圾回收之后,Dalvik 遍歷堆并查找未使用的頁面,然后使用 madvise 將這些頁面回傳給內核,因此,大資料塊的配對分配和解除分配應該使所有(或幾乎所有)使用的物理記憶體被回收,但是,從較小分配量中回收記憶體的效率要低得多,因為用于較小分配量的頁面可能仍在與其他尚未釋放的資料塊共享,
3.5 限制應用記憶體
為了維持多任務環境的正常運行,Android 會為每個應用的堆大小設定硬性上限,不同設備的確切堆大小上限取決于設備的總體可用 RAM 大小,如果您的應用在達到堆容量上限后嘗試分配更多記憶體,則可能會收到 OutOfMemoryError,
在某些情況下,例如,為了確定在快取中保存多少資料比較安全,您可能需要查詢系統以確定當前設備上確切可用的堆空間大小,您可以通過呼叫 getMemoryClass() 向系統查詢此數值,此方法回傳一個整數,表示應用堆的可用兆位元組數,
3.6 切換應用
當用戶在應用之間切換時,Android 會將非前臺應用保留在快取中,非前臺應用就是指用戶看不到或未運行前臺服務(如音樂播放)的應用,例如,當用戶首次啟動某個應用時,系統會為其創建一個行程;但是當用戶離開此應用時,該行程不會退出,系統會將該行程保留在快取中,如果用戶稍后回傳該應用,系統就會重復使用該行程,從而加快應用切換速度,
如果您的應用具有快取的行程且保留了目前不需要的資源,那么即使用戶未使用您的應用,它也會影響系統的整體性能,當系統資源(如記憶體)不足時,它將會終止快取中的行程,系統還會考慮終止占用最多記憶體的行程以釋放 RAM,
相關推薦
Java 垃圾回收(GC)
Java 類加載器
Android Apk 的打包程序
Android 65536解決方案
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/303622.html
標籤:其他
