Android性能優化之虛擬機調優
介紹完 深入學習Android:虛擬機 & 運行時 之後,很多小夥伴問我,你描述的這些知識結構看起來艱深晦澀高大上,實際工作中能有多大用途呢?今天我就簡單舉個例子。
眾所周知,我們的Android App運行在Java虛擬機之上,而Java是一門帶GC的語言。在虛擬機進行垃圾回收的時候,要做一件很形象的事叫做STW(stop the world);也就是說,為了回收那些不再使用的對象,虛擬機必須要停止所有的線程來進行必要的工作。雖說這一點在ART運行時上得到了很大的改善,但是GC的存在對App運行時的性能始終有著微妙的影響。如果你觀察過手機輸入的日誌,一定會看到類似如下的內容:
12-23 18:46:06.470 28643-28658/? I/art: Background sticky concurrent mark sweep GC freed 14069(1392KB) AllocSpace objects, 7(112KB) LOS objects, 4% free, 32MB/33MB, paused 5.032ms total 49.071ms at GCDaemon thread CareAboutPauseTimes 1
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.502ms at 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函數的最後有這麼一句話:
if (zygote) {n runtime.start("com.android.internal.os.ZygoteInit", args);n} else if (className) {n}n
最終調用到了 AndroidRuntime.cpp 的 `start` 函數,而這個函數中最重要的一步就是啟動虛擬機:
JNIEnv *env;nif (startVm(&mJavaVM, &env) != 0) {n return;n}n
這個函數相當之長,不過都是解析虛擬機啟動的參數,比如堆大小等等;這些參數的來源最終是從/system/build.prop中讀取的。探究android:largeHeap - 技術小黑屋 這篇文章對一些重要的參數做了說明,這些參數對虛擬機非常重要,後面我們會見到。解析參數完畢之後,最終調用 `JNI_CreateJavaVM` 來真正創建Java虛擬機。這個介面是Android虛擬機定義的三個介面這一;它的具體是現在 jni_internal.cc;JNI_CreateJavaVM 這個函數在拿到虛擬機的相關參數之後,就直接創建了Android運行時:
if (!Runtime::Create(options, ignore_unrecognized)) {n return JNI_ERR;n }n
Runtime的創建非常複雜,其中,跟GC相關的是,App的堆空間Heap對象被創建出來了;Heap的構造函數接受了一大堆參數,這些參數對於GC有著重大的影響,如果要調整GC的策略,從這裡入手,是比較靠譜的。
heap_ = new gc::Heap(options->heap_initial_size_,n options->heap_growth_limit_,n options->heap_min_free_,n options->heap_max_free_,n options->heap_target_utilization_,n options->foreground_heap_growth_multiplier_,n options->heap_maximum_size_,n
其中 heap_initial_size_ 是堆的初始大小,heap_growth_limit_是堆增長的最大限制,heap_min_free_以及heap_max_free_ 是什麼呢?詳細的用途見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_min_free_ 以及 heap_max_free_,就能很大程度上影響GC的過程。
如何調整這兩個參數的大小呢?拿到Heap對象的指針,找到這兩個參數的偏移量,直接修改內存即可。這裡稍微需要一點C++內存布局的知識;至於如何拿到Heap對象的指針,只有去源碼裡面尋找答案了。這裡我給出最終的實現代碼:
void modifyHeap(unsigned size) {nn // JavaVMExt指針 可以從JNI_OnLoad中拿到 n JavaVMExt *vmExt = (JavaVMExt *) g_javaVM;n if (vmExt->runtime == NULL) {n return;n }nn char *runtime_ptr = (char *) vmExt->runtime;n void **heap_pp = (void **) (runtime_ptr + 188);n char *c_heap = (char *) (*heap_pp);nn char *min_free_offset = c_heap + 532;n char *max_free_offset = min_free_offset + 4;n char *target_utilization_offset = max_free_offset + 4;nn size_t *min_free_ = (size_t *) min_free_offset;n size_t *max_free_ = (size_t *) max_free_offset;nn *min_free_ = 1024 * 1024 * 2;n *max_free_ = 1024 * 1024 * 8;n}n
修改之後啟動過程中內存佔用如下,可以看到我們的目的已經達到:
順便說明一下,上面的代碼沒有考慮任何的可移植性和適配性,只起演示作用。真正投入使用是一個體力活:其一,我們依賴了某特定Android版本某個類的內存布局,其中的成員變數的偏移量可能不同版本不同;其二,這個 min_free_ 以及 max_free_ 具體調整為多大,跟手機的物理內存,App使用的內存,手機配置的初始堆大小等等因素密切相關;調整一個合適的參數需要花費一些時間,Android機型如此之多,這裡需要一些小技巧。
不知道上面這個例子有木有讓你感受到深入系統底層,那種呼風喚雨無所不能的快感?可能很多人覺得我們都是寫寫if else而已,調節面改動畫寫業務已經夠了;但我想說明的是,深入學習系統原理是非常有好處的,它可以賦予你在應用層永遠無法擁有的能力。
另外留個作業,我們上面提到觀察GC的次數,除了使用debug模式下用工具觀察,能不能用代碼監聽到呢?本文主要說明了虛擬機運行時等native層的重要性,而這個答案可以在Java Framework中找到 ^_^
推薦閱讀:
※如何評價技德科技(JIDE)推出的 Remix Android 平板電腦?
※你在 iPhone / Android 上最常用的日程管理軟體是什麼?
※Android輔助功能---全局手勢放大
※讓這款壁紙應用來發現你隱藏已久的藝術氣息
※宏碁與阿里巴巴聲稱因谷歌直接壓力,臨時取消新手機發布會,這是真的嗎?還是兩者的炒作?