Android性能优化之虚拟机调优
介紹完?深入學(xué)習(xí)Android:虛擬機&運行時?之后,很多小伙伴問我,你描述的這些知識結(jié)構(gòu)看起來艱深晦澀高大上,實際工作中能有多大用途呢?今天我就簡單舉個例子。
眾所周知,我們的Android App運行在Java虛擬機之上,而Java是一門帶GC的語言。在虛擬機進行垃圾回收的時候,要做一件很形象的事叫做STW(stop the world);也就是說,為了回收那些不再使用的對象,虛擬機必須要停止所有的線程來進行必要的工作。雖說這一點在ART運行時上得到了很大的改善,但是GC的存在對App運行時的性能始終有著微妙的影響。如果你觀察過手機輸入的日志,一定會看到類似如下的內(nèi)容:
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是有代價的。有很多有關(guān)性能優(yōu)化的文章提到GC,會花長篇大論講述垃圾回收的過程以及原理,但所做的策略無非就是「不要創(chuàng)建不必要的對象」,「避免內(nèi)存泄漏」最終就提到MAT,LeakCanary等工具的使用上去了;我只能說這很蒼白無力——寫出這樣的代碼、學(xué)會使用工具應(yīng)該是基本要求。
雖說Android也支持NDK開發(fā),但是我們不可能把所有代碼全用C++重寫吧?那么,我們有沒有辦法能影響GC的策略,使得GC盡量減少呢?答案是肯定的。原理在于Android的進程機制——每一個App都有一個單獨的虛擬機實例,在App自己的進程空間,我們有相當(dāng)大的主動權(quán)。
我舉個簡單的例子。(下面的內(nèi)容基于Android 5.1系統(tǒng),所有的原理以及代碼不保證能在其他系統(tǒng)版本甚至ROM上工作)
Android上所有的App進程都從Zygote進程fork而來,App子進程采用copy on write機制共享了Zygote進程的進程空間;其中Android虛擬機以及運行時的創(chuàng)建在Android系統(tǒng)啟動,創(chuàng)建Zygote進程的時候已經(jīng)完成了。垃圾回收機制是虛擬機的一部分,因此,我們先從Zygote進程的啟動過程談起。
我們知道,Android系統(tǒng)是基于Linux內(nèi)核的,而在Linux系統(tǒng)中,所有的進程都是init進程的子孫進程,Zygote進程也不例外,它是在系統(tǒng)啟動的過程,由init進程創(chuàng)建的。在系統(tǒng)啟動腳本system/core/rootdir/init.rc文件中,我們可以看到啟動Zygote進程的腳本命令:
service zygote /system/bin/app_process -Xzygote /system/bin –zygote –start-system-server
也就是說init進程通過執(zhí)行 /system/bin/app_process 這個可執(zhí)行文件來創(chuàng)建zygote進程;app_process的源碼可見?這里;在main函數(shù)的最后有這么一句話:
| 1 2 3 | if (zygote) { runtime.start("com.android.internal.os.ZygoteInit", args); } else if (className) { |
最終調(diào)用到了AndroidRuntime.cpp?的start函數(shù),而這個函數(shù)中最重要的一步就是啟動虛擬機:
| 1 2 3 4 | JNIEnv* env; if (startVm(&mJavaVM, &env) != 0) { return; } |
這個函數(shù)相當(dāng)之長,不過都是解析虛擬機啟動的參數(shù),比如堆大小等等;探究largeHeap?這篇文章對一些重要的參數(shù)做了說明,這些參數(shù)對虛擬機非常重要,后面我們會見到。解析參數(shù)完畢之后,最終調(diào)用JNI_CreateJavaVM來真正創(chuàng)建Java虛擬機。這個接口是Android虛擬機定義的三個接口這一,dalvik能切換到art很大程度上與這個有關(guān)。它的具體是現(xiàn)在?jni_internal.cc;JNI_CreateJavaVM 這個函數(shù)在拿到虛擬機的相關(guān)參數(shù)之后,就直接創(chuàng)建了Android運行時:
| 1 2 3 | if (!Runtime::Create(options, ignore_unrecognized)) { return JNI_ERR; } |
Runtime的創(chuàng)建非常復(fù)雜,其中,跟GC相關(guān)的是,App的堆空間被創(chuàng)建出來了;Heap的構(gòu)造函數(shù)接受了一大堆參數(shù),這些參數(shù)對于GC有著重大的影響,如果要調(diào)整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?是什么呢?詳細(xì)的用途見?Android ART GC之GrowForUtilization的分析?簡單來說就是,Android系統(tǒng)為了保證堆的利用效率,減少堆中的內(nèi)存碎片;每次執(zhí)行GC回收到一些內(nèi)存之后,會對堆大小進行調(diào)整。比如說你進入了一個圖片非常多的頁面,這時候申請了100M內(nèi)存,當(dāng)你退出這個頁面的時候,這100M自然就被回收了,成為了空閑內(nèi)存;但是系統(tǒng)為了防止浪費,并不會把這100M的空閑內(nèi)存全部留給你,而是做一個調(diào)整。而具體調(diào)整到多大,則與heap_min_free_,?heap_max_free_?以及?heap_target_utilization_?相關(guān)。
說到這里,原理性的部分已經(jīng)解釋完了;除了流程稍微復(fù)雜,也沒有什么難點。那么這個堆,跟我們的啟動性能優(yōu)化有什么關(guān)系呢?
在Android App的啟動過程中,進程占用的內(nèi)存在一段時間內(nèi)是持續(xù)上漲的;假設(shè)堆的初始大小為8M,啟動過程中的占用內(nèi)存峰值30M;啟動過程的進行中,伴隨著大量臨時對象的創(chuàng)建,它們朝生夕死,不久就被回收掉:
如上圖,這是某次啟動過程中某App的內(nèi)存占用情況;我們看到了有很多小折線,專業(yè)術(shù)語叫做內(nèi)存抖動;原因呢,也很明顯——有大量的臨時對象被創(chuàng)建。怎么解決?有人說,不要創(chuàng)建大量的臨時對象。道理我都懂,可是做不到。對于很多大型App來說,啟動的過程是相當(dāng)復(fù)雜的,而很多操作也不能簡單滴去掉。那么問題來了,30M并不是一個很大的數(shù)字,為什么系統(tǒng)如此恐慌,還需要不停滴回收內(nèi)存呢?
有一種冷,叫做你媽媽覺得你冷。垃圾回收并不是說有垃圾了才去回收,而是只要系統(tǒng)覺得你需要回收垃圾就會進行。
那么,能不能在啟動過程中讓堆保持持續(xù)增長而不進行GC呢?畢竟,30M并不會造成什么OOM。是什么原因?qū)е孪到y(tǒng)沒有這么做?答案是空閑內(nèi)存。比如說一開始堆有8M,隨著啟動過程的進行,堆增長到了24M;這時候執(zhí)行了一次GC,回收掉了8M內(nèi)存,也是堆回到了16M;我們還有8M的空閑內(nèi)存。系統(tǒng)就會說,小伙子,你占這么多空閑內(nèi)存干嘛呀?來媽媽幫你保管,于是你就只剩下2M的空閑內(nèi)存了。但顯然App使用的堆內(nèi)存很快就會超過18M,于是又引發(fā)一系列GC以及堆大小調(diào)整,周而復(fù)始直至啟動完成內(nèi)存平穩(wěn)。至此,我們的結(jié)論已經(jīng)很明顯:
如果我們能夠調(diào)整 heap_minfree?以及 heap_maxfree,就能很大程度上影響GC的過程
如何調(diào)整這兩個參數(shù)的大小呢?拿到Heap對象的指針,找到這兩個參數(shù)的偏移量,直接修改內(nèi)存即可?這里稍微需要一點C++內(nèi)存布局的知識;至于如何拿到Heap對象的指針,只有去源碼里面尋找答案了。這里我給出最終的實現(xiàn)代碼:
| 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; } |
修改之后啟動過程中內(nèi)存占用如下,可以看到我們的目的已經(jīng)達(dá)到:
順便說明一下,上面的代碼沒有考慮任何的可移植性和適配性,只起演示作用。真正投入使用是一個體力活:其一,我們依賴了某特定Android版本某個類的內(nèi)存布局,其中的成員變量的偏移量可能不同版本不同;其二,這個 minfree?以及 maxfree?具體調(diào)整為多大,跟手機的物理內(nèi)存,App使用的內(nèi)存,手機配置的初始堆大小等等因素密切相關(guān);調(diào)整一個合適的參數(shù)需要花費一些時間,Android機型如此之多,這里需要一些小技巧。
不知道上面這個例子有木有讓你感受到深入系統(tǒng)底層,那種呼風(fēng)喚雨無所不能的快感?可能很多人覺得我們都是寫寫if else而已,調(diào)節(jié)面改動畫寫業(yè)務(wù)已經(jīng)夠了;但我想說明的是,深入學(xué)習(xí)系統(tǒng)原理是非常有好處的,它可以賦予你在應(yīng)用層永遠(yuǎn)無法擁有的能力。
另外留個作業(yè),我們上面提到觀察GC的次數(shù),除了使用debug模式下用工具觀察,能不能用代碼監(jiān)聽到呢?本文主要說明了虛擬機運行時等native層的重要性,而這個答案可以在Java Framework中找到 ^_^
原文: http://weishu.me/2016/12/23/dive-into-android-optimize-vm-heap/
總結(jié)
以上是生活随笔為你收集整理的Android性能优化之虚拟机调优的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android插件化原理解析——Cont
- 下一篇: Android中关于cpu/cpuset