Android性能优化之虚拟机调优
介紹完?深入學習Android:虛擬機&運行時?之后,很多小伙伴問我,你描述的這些知識結構看起來艱深晦澀高大上,實際工作中能有多大用途呢?今天我就簡單舉個例子。
眾所周知,我們的Android App運行在Java虛擬機之上,而Java是一門帶GC的語言。在虛擬機進行垃圾回收的時候,要做一件很形象的事叫做STW(stop the world);也就是說,為了回收那些不再使用的對象,虛擬機必須要停止所有的線程來進行必要的工作。雖說這一點在ART運行時上得到了很大的改善,但是GC的存在對App運行時的性能始終有著微妙的影響。如果你觀察過手機輸入的日志,一定會看到類似如下的內容:
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函數的最后有這么一句話:
| 1 2 3 | if (zygote) { runtime.start("com.android.internal.os.ZygoteInit", args); } else if (className) { |
最終調用到了AndroidRuntime.cpp?的start函數,而這個函數中最重要的一步就是啟動虛擬機:
| 1 2 3 4 | JNIEnv* env; if (startVm(&mJavaVM, &env) != 0) { return; } |
這個函數相當之長,不過都是解析虛擬機啟動的參數,比如堆大小等等;探究largeHeap?這篇文章對一些重要的參數做了說明,這些參數對虛擬機非常重要,后面我們會見到。解析參數完畢之后,最終調用JNI_CreateJavaVM來真正創建Java虛擬機。這個接口是Android虛擬機定義的三個接口這一,dalvik能切換到art很大程度上與這個有關。它的具體是現在?jni_internal.cc;JNI_CreateJavaVM 這個函數在拿到虛擬機的相關參數之后,就直接創建了Android運行時:
| 1 2 3 | if (!Runtime::Create(options, ignore_unrecognized)) { return JNI_ERR; } |
Runtime的創建非常復雜,其中,跟GC相關的是,App的堆空間被創建出來了;Heap的構造函數接受了一大堆參數,這些參數對于GC有著重大的影響,如果要調整GC的策略,從這里入手,是比較靠譜的。
| 1 2 3 4 5 6 7 8 | heap_ = new gc::Heap(options->heap_initial_size_, options->heap_growth_limit_, options->heap_min_free_, options->heap_max_free_, options->heap_target_utilization_, options->foreground_heap_growth_multiplier_, options->heap_maximum_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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | void modifyHeap(unsigned size) { // JavaVMExt指針 可以從JNI_OnLoad中拿到 JavaVMExt * vmExt = (JavaVMExt *)g_javaVM; if (vmExt->runtime == NULL) { return; } char* runtime_ptr = (char*) vmExt->runtime; void** heap_pp = (void**)(runtime_ptr + 188); char* c_heap = (char*) (*heap_pp); char* min_free_offset = c_heap + 532; char* max_free_offset = min_free_offset + 4; char* target_utilization_offset = max_free_offset + 4; size_t* min_free_ = (size_t*) min_free_offset; size_t* max_free_ = (size_t*) max_free_offset; *min_free_ = 1024 * 1024 * 2; *max_free_ = 1024 * 1024 * 8; } |
修改之后啟動過程中內存占用如下,可以看到我們的目的已經達到:
順便說明一下,上面的代碼沒有考慮任何的可移植性和適配性,只起演示作用。真正投入使用是一個體力活:其一,我們依賴了某特定Android版本某個類的內存布局,其中的成員變量的偏移量可能不同版本不同;其二,這個 minfree?以及 maxfree?具體調整為多大,跟手機的物理內存,App使用的內存,手機配置的初始堆大小等等因素密切相關;調整一個合適的參數需要花費一些時間,Android機型如此之多,這里需要一些小技巧。
不知道上面這個例子有木有讓你感受到深入系統底層,那種呼風喚雨無所不能的快感?可能很多人覺得我們都是寫寫if else而已,調節面改動畫寫業務已經夠了;但我想說明的是,深入學習系統原理是非常有好處的,它可以賦予你在應用層永遠無法擁有的能力。
另外留個作業,我們上面提到觀察GC的次數,除了使用debug模式下用工具觀察,能不能用代碼監聽到呢?本文主要說明了虛擬機運行時等native層的重要性,而這個答案可以在Java Framework中找到 ^_^
原文: http://weishu.me/2016/12/23/dive-into-android-optimize-vm-heap/
總結
以上是生活随笔為你收集整理的Android性能优化之虚拟机调优的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android插件化原理解析——Cont
- 下一篇: Android中关于cpu/cpuset