Android效能優化(31)---虛擬機器調優
12-23 18:46:07.300 28643-28658/? I/art: Background sticky concurrent mark sweep GC freed 15442(1400KB) AllocSpace objects, 8(128KB) LOS objects, 4% free, 32MB/33MB, paused 10.356ms total 53.023ms at GCDaemon thread CareAboutPauseTimes 1
12-23 18:46:12.250 28643-28658/? I/art: Background partial concurrent mark sweep GC freed 28723(1856KB) AllocSpace objects, 6(92KB) LOS objects, 11% free, 31MB/35MB, paused 2.380ms total 108.502msat GCDaemon thread CareAboutPauseTimes 1
上面的日誌反映一個事實:GC是有代價的。有很多有關效能優化的文章提到GC,會花長篇大論講述垃圾回收的過程以及原理,但所做的策略無非就是「不要建立不必要的物件」,「避免記憶體洩漏」最終就提到MAT,LeakCanary等工具的使用上去了;我只能說這很蒼白無力——寫出這樣的程式碼、學會使用工具應該是基本要求。
雖說Android也支援NDK開發,但是我們不可能把所有程式碼全用C++重寫吧?那麼,我們有沒有辦法能影響GC的策略,使得GC儘量減少呢?答案是肯定的。原理在於Android的程序機制——每一個App都有一個單獨的虛擬機器例項,在App自己的程序空間,我們有相當大的主動權。
我舉個簡單的例子。(下面的內容基於Android 5.1系統,所有的原理以及程式碼不保證能在其他系統版本甚至ROM上工作)
Android上所有的App程序都從Zygote程序fork而來,App子程序採用copy on write機制共享了Zygote程序的程序空間;其中Android虛擬機器以及執行時的建立在Android系統啟動,建立Zygote程序的時候已經完成了。垃圾回收機制是虛擬機器的一部分,因此,我們先從Zygote程序的啟動過程談起。
我們知道,Android系統是基於Linux核心的,而在Linux系統中,所有的程序都是init程序的子孫程序,Zygote程序也不例外,它是在系統啟動的過程,由init程序建立的。在系統啟動指令碼system/core/rootdir/init.rc檔案中,我們可以看到啟動Zygote程序的指令碼命令:
service zygote /system/bin/app_process -Xzygote /system/bin –zygote –start-system-server
也就是說init程序通過執行 /system/bin/app_process 這個可執行檔案來建立zygote程序;app_process的原始碼可見 這裡;在main函式的最後有這麼一句話:
1 | if (zygote) { |
最終呼叫到了AndroidRuntime.cpp 的start
函式,而這個函式中最重要的一步就是啟動虛擬機器:
1 | JNIEnv* env; |
這個函式相當之長,不過都是解析虛擬機器啟動的引數,比如堆大小等等;探究largeHeap 這篇文章對一些重要的引數做了說明,這些引數對虛擬機器非常重要,後面我們會見到。解析引數完畢之後,最終呼叫JNI_CreateJavaVM
來真正建立Java虛擬機器。這個介面是Android虛擬機器定義的三個介面這一,dalvik能切換到art很大程度上與這個有關。它的具體是現在 jni_internal.cc;JNI_CreateJavaVM 這個函式在拿到虛擬機器的相關引數之後,就直接建立了Android執行時:
1 | if (!Runtime::Create(options, ignore_unrecognized)) { |
Runtime的建立非常複雜,其中,跟GC相關的是,App的堆空間被創建出來了;Heap的建構函式接受了一大堆引數,這些引數對於GC有著重大的影響,如果要調整GC的策略,從這裡入手,是比較靠譜的。
1 | heap_ = new gc::Heap(options->heap_initial_size_, |
其中 heap_initialsize 是堆的初始大小,heap_growthlimit是堆增長的最大限制,heap_minfree以及heap_maxfree 是什麼呢?詳細的用途見 Android ART GC之GrowForUtilization的分析 簡單來說就是,Android系統為了保證堆的利用效率,減少堆中的記憶體碎片;每次執行GC回收到一些記憶體之後,會對堆大小進行調整。比如說你進入了一個圖片非常多的頁面,這時候申請了100M記憶體,當你退出這個頁面的時候,這100M自然就被回收了,成為了空閒記憶體;但是系統為了防止浪費,並不會把這100M的空閒記憶體全部留給你,而是做一個調整。而具體調整到多大,則與heap_min_free_
, heap_max_free_
以及 heap_target_utilization_
相關。
說到這裡,原理性的部分已經解釋完了;除了流程稍微複雜,也沒有什麼難點。那麼這個堆,跟我們的啟動效能優化有什麼關係呢?
在Android App的啟動過程中,程序佔用的記憶體在一段時間內是持續上漲的;假設堆的初始大小為8M,啟動過程中的佔用記憶體峰值30M;啟動過程的進行中,伴隨著大量臨時物件的建立,它們朝生夕死,不久就被回收掉:
如上圖,這是某次啟動過程中某App的記憶體佔用情況;我們看到了有很多小折線,專業術語叫做記憶體抖動;原因呢,也很明顯——有大量的臨時物件被建立。怎麼解決?有人說,不要建立大量的臨時物件。道理我都懂,可是做不到。對於很多大型App來說,啟動的過程是相當複雜的,而很多操作也不能簡單滴去掉。那麼問題來了,30M並不是一個很大的數字,為什麼系統如此恐慌,還需要不停滴回收記憶體呢?
有一種冷,叫做你媽媽覺得你冷。垃圾回收並不是說有垃圾了才去回收,而是隻要系統覺得你需要回收垃圾就會進行。
那麼,能不能在啟動過程中讓堆保持持續增長而不進行GC呢?畢竟,30M並不會造成什麼OOM。是什麼原因導致系統沒有這麼做?答案是空閒記憶體。比如說一開始堆有8M,隨著啟動過程的進行,堆增長到了24M;這時候執行了一次GC,回收掉了8M記憶體,也是堆回到了16M;我們還有8M的空閒記憶體。系統就會說,小夥子,你佔這麼多空閒記憶體幹嘛呀?來媽媽幫你保管,於是你就只剩下2M的空閒記憶體了。但顯然App使用的堆記憶體很快就會超過18M,於是又引發一系列GC以及堆大小調整,周而復始直至啟動完成記憶體平穩。至此,我們的結論已經很明顯:
如果我們能夠調整 heap_minfree 以及 heap_maxfree,就能很大程度上影響GC的過程
如何調整這兩個引數的大小呢?拿到Heap物件的指標,找到這兩個引數的偏移量,直接修改記憶體即可 這裡稍微需要一點C++記憶體佈局的知識;至於如何拿到Heap物件的指標,只有去原始碼裡面尋找答案了。這裡我給出最終的實現程式碼:
1 | void modifyHeap(unsigned size) { |
修改之後啟動過程中記憶體佔用如下,可以看到我們的目的已經達到:
順便說明一下,上面的程式碼沒有考慮任何的可移植性和適配性,只起演示作用。真正投入使用是一個體力活:其一,我們依賴了某特定Android版本某個類的記憶體佈局,其中的成員變數的偏移量可能不同版本不同;其二,這個 minfree 以及 maxfree 具體調整為多大,跟手機的實體記憶體,App使用的記憶體,手機配置的初始堆大小等等因素密切相關;調整一個合適的引數需要花費一些時間,Android機型如此之多,這裡需要一些小技巧。
不知道上面這個例子有木有讓你感受到深入系統底層,那種呼風喚雨無所不能的快感?可能很多人覺得我們都是寫寫if else而已,調節面改動畫寫業務已經夠了;但我想說明的是,深入學習系統原理是非常有好處的,它可以賦予你在應用層永遠無法擁有的能力。
另外留個作業,我們上面提到觀察GC的次數,除了使用debug模式下用工具觀察,能不能用程式碼監聽到呢?本文主要說明了虛擬機器執行時等native層的重要性,而這個答案可以在Java Framework中找到 ^_^