Android内存分析和调优
第一層 Procrank
很粗略的,可以使用"adb shell procrank",結果類似于PID ? ?Vss ? ? ? ?Rss ? ? ? ?Pss ? ? ? Uss ? ? ?cmdline
......
2319 42068K 42032K 13536K 7028K com.xxx
......
該命令可以列出當前系統所有進程的內存占用情況。
PID是進程ID。
Vss是占用的虛擬內存,如果沒有映射實際的內存也算進來。
Rss是占用的物理內存。是共享內存+私有內存。因為共享內存是多個進程共用的,所以存在重復計算。
Pss是占用的私有內存加上平分的共享內存。例如一塊1M的共享內存被兩個進程共享,那每個進程分500K。各進程的Pss相加基本等于實際被使用的物理內存,所以這個經常是最重要的參數。
Uss是私有內存。
cmdline可以看做是apk包名。
通過procrank,只能很宏觀的橫向比較不同的應用。如果要更細致的了解具體內存是如何使用,則需要進入
第二層 dumpsys meminfo
命令“adb shell dumpsys meminfo package.name”。在4.0 ICS(或者3.0 HoneyComb)之后的系統上,會看到類似下面的輸出
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Shared ? Private ?Heap ? ?Heap ? ? Heap
? ? ? ? ? ? ? ? ? ? ? Pss ? ? ?Dirty ? ? ?Dirty ? ? Size ? ? Alloc ? ? ?Free
? ? ? ? ? ? ? ? ? ? ? ------ ? ------ ? ?------ ? ? ------ ?------ ? ? ------
Native ? ? ? ? ? ?16 ? ? ? ?8 ? ? ? ? ? 16 ? ? ? ?3416 ? 3300 ? ? 79
Dalvik ? ? ? ? ? ?3884 ? ?10592 ? 3580 ? ?9560 ? 9022 ? ? 538
Cursor ? ? ? ? ? ?0 ? ? ? ? ?0 ? ? ? ? ? 0?
Ashmem ? ? ? ?0 ? ? ? ? ? 0 ? ? ? ? ? 0?
Other dev ? ? ? 5110 ? ?10244 ? 0?
.so mmap ? ? ? 640 ? ? 1948 ? ? ?396?
.jar mmap ? ? ?0 ? ? ? ? ?0 ? ? ? ? ? 0?
.apk mmap ? ? 68 ? ? ? ?0 ? ? ? ? ? 0?
.ttf mmap ? ? ? 817 ? ? ?0 ? ? ? ? ? 0?
.dex mmap ? ? 411 ? ? ?0 ? ? ? ? ? 0?
Other mmap ? 55 ? ? ? ?16 ? ? ? ? 32?
Unknown ? ? ? ?2404 ? ? 660 ? ? ? 2388?
TOTAL ? ? ? ? ? ?13405 ?23468 ? 6412 ? ?12976 12322 617
(如果使用2.3或之前的版本,結果會粗糙一些,很多都被歸入了Other,但基本結構是一樣的)
stacktrace上有個經常被搜到的帖子對這個格式有說明,雖然針對的是android 2.3格式,但讀后非常有收獲。
但仍有很多疑問沒有解答,例如針對上面的例子,為什么Native heap size那么大,但Pss卻那么小?占用內存比較多的Other dev是什么?Unknown又有哪些?等等。
要理解這些,需要知道這個report是如何生成的。實際上,生成report的代碼是android的android_os_Debug.cpp。
從中我們可以發現,上面列表的數據是由三種方式獲取的:
1. Pss/Shared Dirty/Private Dirty三列是讀取了/proc/process-id/smaps文件獲取的。它會對每個虛擬內存塊進行解析,然后生成數據。
2. Native Heap Size/Alloc/Free三列是使用C函數mallinfo得到的。
3. Dalvik Heap Size/Alloc/Free并非該cpp文件產生,而是android的Debug類生成。
后面兩個Heap的獲取比較簡單,我唯一的疑惑是為什么有free的?我的理解是無論是c的malloc還是java的new,最后都是通過mmap系統調用進行內存分配的。而mmap必須以頁的4K為單位。所以如果一次一次只需要malloc 2K,則剩下的2K是free的。如果下次再malloc 2K,可以仍然使用上次mmap剩余的2K內存。
至于smaps文件,我們可以通過adb shell cat /proc/process-id/smaps來查看(需要root)。這是個普通的linux文件,描述了進程的虛擬內存區域(vm area)的具體信息。每次mmap一般都會生成一個vm area。
在Android上,一個更加方便的命令是adb shell showmap -a process-id。
第三層 adb shell showmap
該命令也是讀取smaps文件,但結果細化的具體的vm area。
該命令輸出的每行表示一個vm area,列出了該vm area的start addr, end addr, Vss, Rss, Pss, shared clean, shared dirty, private clean, private dirty,object。?
第二層的dumpsys meminfo其實就是讀取這些數據,然后分類(native, dalvik, .so map, etc.)統計生成。
start addr和end addr表示進程空間的起止虛擬地址。
Vss,Rss,Pss跟前面說的一樣。
Object可以看做mmap的文件名。
Shared clean,按字面意思,表示共享的干凈的數據。共享表示多個進程的虛擬地址可以都指向這塊物理空間,表示多個進程共享的so庫。為什么這里說是多個進程共享的so而不是所有的so呢?
關于so庫的加載,我一直覺得是mmap帶MAP_SHARED參數,但看了memory_faq,才知道是MAP_PRIVATE。如果使用showmap命令查看vm area,會發現有的so的內存都屬于Shared clean,而有的so則屬于private clean。前者一般是當前進程特有的so,而后者一般是通用的so。后來看了對mmap的各種參數的實驗(很贊實踐精神),才知道第一次以MAP_PRIVATE mmap so,內存都是private clean的。如果另外一個進程mmap了同一個so,那該vm area就變成shared clean了。
Private clean,包括該進程私有的干凈的內存。包括前面說的該進程獨自使用的so和進程的二進制代碼段。
Clean內存的好處是在內存緊張時,可以釋放物理內存。因為是clean的,所以不需要寫回到disk,只需要下次讀取該內存(導致缺頁錯誤)時再從disk讀入。
Private dirty,表示該進程私有的不跟disk數據一致的內存段。例如堆(heap),棧(stack),bss段。關于bss段,因為在elf文件為了節約控件沒有賦值,所以在加載到內存時賦值為0,于是跟disk就不一致了。在showmap結果中,會發現幾乎每個so都有一個顯示位[bss]的private dirty段。數據段我估計是private clean的,因為elf文件是有初值的。
Shared dirty開始我一直搞不清楚。后來看了Dalvik vm internal這個video(slides),才明白了些。對于普通的linux進程,當父進程fork子進程時,父進程的虛擬內存區域都會”復制“一份到子進程中。這里”復制“加引號,是因為為了節省內存,也為了減少內存拷貝的時間,使用的是copy-on-write的方法。當子進程對private dirty的堆,棧,bss沒有修改時,則是父子進程share這份dirty(因為跟disk沒法映射)數據。如果發生改變,則會修改為private dirty。所以android有zygote進程,是所有android apps進程的父進程,在其中會加載resource等資源(下文會看到,最簡單的應該也有大概5M resource,例如圖片),這些資源都是只讀的。具體的apps繼承了這些shared dirty的數據,因為不修改它們,所以也不用分配多余的內存空間。
由于android使用的linux沒有swap分區,所以dirty的數據必須常駐內存。所以dumpsys meminfo會把private dirty和shared dirty重點列出來,這也是我們優化內存的重點。
現在可以回答一個前面提到的問題,為什么Native Heap(根據mallinfo系統調用得到)很大而Native Pss(根據swaps得到)很小。我覺得這是dumpsys meminfo的一個bug。根據android_os_Debug.cpp的代碼,object名字是[heap]的段被認為是native heap。這在2.3是正確的,但在4.0之后,[heap]為名字的段卻很小(只有幾K)。同時,我卻發現有大量的[anon]的區域。我認為anon是anonymous的縮寫。malloc一般是通過mmap來分配內存的,而參數是MAP_ANONYMOUS。所以我覺得這些[anon]是native heap。從大小上看,現在這些[anon]被看做是Unkown的一部分,也跟hative heap的大小差不多。
在dumpsys meminfo結果的其他值比較大的行,.so表示映射的so庫(vm area行的object名稱包含.so字樣),.dex表示映射的.dex文件(dalvik的虛擬機二進制碼),Other dev表示映射其他的/dev的(dalvik的heap也是映射到特殊的/dev上)。加上native和dalvik的heap,下次寫如何具體分析這五項。
在前文中討論了如果使用adb shell procrank, dumpsys meminfo和showmaps分析進程的內存占用情況。
本文將繼續細化,具體分析導致內存過大的dalvik heap。
Dalvik heap分析和優化
Dalkvik heap是最常見的android應用內存優化的對象。
通過上文的分析,我們可以通過adb shell的命令,知道用了多少dalvik heap。在ADT的eclipse的DDMS視圖,可以更細致的查看這些內存用到什么地方。
參考DDMS使用說明(搜索viewing heap),我們可以首先在devices view中選中一個進程,然后enable "update heap“(不帶紅箭頭的半杯水圖標),之后在heap view中點擊”Cause GC"。這樣子除了Heap Size, Allocated, Freed,還可以看到data object,class object,和n-byte array分別占用的內存大小。
不過真心說,這個還是太粗糙了,沒法精確到具體的類。此時大名鼎鼎的MAT就派上用場了。
MAT是對java內存鏡像進行分析的工具。所以首先需要導出進程的內存鏡像,可以在DDMS上的device view點擊Dump HPROF file(帶紅箭頭的半杯水圖標),生成hprof文件。因為android的文件格式跟通用的java的hprof格式不一樣,還需要通過hprof-conv命令來轉換。然后就可以用MAT來打開。
看起來挺麻煩的。事實上,現在MAT的eclipse插件可以把上面的工具一鍵完成。只需要點擊Dump HPROF file圖標,然后MAT插件就會自動轉換格式,并且在eclipse中打開分析結果。eclipse中還專門有個Memory Analysis視圖,可以更詳細的查看MAT的分析結果。
MAT可以根據內存鏡像,以可視化的方式告訴我們哪個類,哪個對象分配了多少內存。但如果只是這樣,用處就沒那么大了。因為不像c++的對象本身可以存放大量內存,java的對象成員都是些引用。真正的內存都在堆上,看起來是一堆原生的byte[], char[], int[]。所以我們如果只看對象本身的內存,那么數量都很小。我們稱之位shallow heap。
于是MAT提出了Retained Heap的概念,它表示如果一個對象被釋放掉,那會因為該對象的釋放而減少引用進而被釋放的所有的對象(包括被遞歸釋放的)所占用的heap大小。于是,如果一個對象的某個成員new了一大塊int數組,那這個int數組也可以計算到這個對象中。相對于shallow heap,Retained heap可以更精確的反映一個對象實際占用的大小(因為如果該對象釋放,retained heap都可以被釋放)。這里要說一下的是,Retained Heap并不總是那么有效。例如我在A里new了一塊內存,賦值給A的一個成員變量。此時我讓B也指向這塊內存。此時,因為A和B都引用到這塊內存,所以A釋放時,該內存不會被釋放。所以這塊內存不會被計算到A或者B的Retained Heap中。為了糾正這點,MAT中的Leading Object(例如A或者B)不一定只是一個對象,也可以是多個對象。此時,(A, B)這個組合的Retained Set就包含那塊大內存了。對應到MAT的UI中,在Histogram中,可以選擇Group By class, superclass or package來選擇這個組。(又開始Histogram中不顯示Retained heap,需要點擊那個計算器的按鈕才會計算出來)。這里最小的粒度是類級別的。
為了計算Retained Memory,MAT引入了Dominator Tree。加入對象A引用B和C,B和C又都引用到D(一個菱形)。此時要計算Retained Memory,A的包括A本身和B,C,D。B和C因為共同引用D,所以他倆的Retained Memory都只是他們本身。D當然也只是自己。我覺得是為了加快計算的速度,MAT改變了對象引用圖,而轉換成一個對象引用樹。在這里例子中,樹根是A,而B,C,D是他的三個兒子。B,C,D不再有相互關系。把引用圖變成引用樹,計算Retained Heap就會非常方便,顯示也非常方便。對應到MAT UI上,在dominator tree這個view中,顯示了每個對象的shallow heap和retained heap。然后可以以該節點位樹根,一步步的細化看看retained heap到底是用在什么地方了。要說一下的是,這種從圖到樹的轉換確實方便了內存分析,但有時候會讓人有些疑惑。本來對象B是對象A的一個成員,但因為B還被C引用,所以B在樹中并不在A下面,而很可能是平級。
為了糾正這點,MAT中點擊右鍵,可以List objects中選擇with outgoing references和with incoming references。這是個真正的引用圖的概念,表示該對象的出節點(被該對象引用的對象)和入節點(引用到該對象的對象)。
另外一個類似的功能是右鍵菜單的Path to GC Roots。GC roots是可能導致GC的節點。這個Path則是從這些GC root節點中的某個到當前對象的最短引用路徑。對這個如何計算不是很確定,我想應該是根據引用樹而不是dominator tree。后面會看到這個功能在非常的有用。
說完工具,下面是具體的減少內存大小。一般要解決兩個問題:內存泄露和釋放暫時不需要的內存。
Java內存泄露歸根結底都是一個原因導致的,應該被釋放的對象被生命期更長的對象引用,所以沒法被GC。這個生命期更長的對象很常見的是static對象,會持續整個進程。在個人實際工作中,我會先用adb shell dumpsys meminfo查看dalvik heap會不會持續增長。如果是,我會在在dominator Tree中按照Retained Memory排序,找出比較大的(經常是Bitmap),然后用Path to GC Roots看看其引用情況。在這個Path中,一般會發現我們app自己包的類,可以分析這個類是不是還是需要的。如果不需要,那說明可能存在內存泄露。此時,在對這個自己包的類查看incoming references。看看到底是哪些引用導致它沒有釋放。用這種方法,會比較快的發現問題。MAT自己也提供了智能的內存分析工具,我沒有用,不好評論。
一個制造內存泄露的很有效的辦法是不斷的切換橫屏和豎屏。現實中很多內存泄露都是因為static的對象指向了Activity對象(作為context傳),而切換橫屏和豎屏會導致Activity重新生成。所以如果有問題,內存很快就會變大。從編碼上講,avoid-memory-leak這篇文章教育我們,在需要context的地方,盡量使用getApplicationContext,而不是Activity本身。
另外一個可以減少內存的方法是刪除臨時不用的內存。編碼中可能是為了內存cache以提高性能,可能只是偷懶,之前場景使用的內存并沒有被釋放掉。這樣子下次再回到這個場景,會快一點;但會可能會占用不少內存。我覺得在android這類內存受限的系統上,還是應該謹慎使用控件換時間的策略。如果想刪除臨時不用的內存,也可以使用mat像監測內存泄露一樣,看看哪些比較大的內存臨時不用卻仍然被引用,然后刪除對其引用。
關于mat的一個小技巧是mat經常發現比較大的內存泄露是圖片,此時如果知道圖片是什么內容就很容易定位到何時導致的內存泄露。這個帖子回答了這個問題。
關于dalvik mat最后再推薦自己看的一個android memory manage?video(slides?,?content,content2)。里面對MAT和內存泄露都有介紹。這個blog也是對二者都有介紹,很好。關于MAT更好的文檔集合在這里,MAT作者寫的。
Native Heap分析和優化
android的DDMS可以幫助查看c++ native heap的使用,但需要一定的配置,而且必須是root的手機。
adb root
adb shell setprop libc.debug.malloc 1
adb shell stop
adb shell start
? ? ? 在很多手機上,即使執行了這些命令,還是看不到結果。原因是很多手機上并沒有安裝debug版本的malloc庫(包括libc_malloc_debug_leak.so?和?libc_malloc_debug_qemu.so)。這篇經常被引用的文章介紹了一種方法。是從供大家刷機用的CyanogenMod?image中提取這兩個文件,然后拷貝到自己的機器上。可以參考那片文章的具體步驟。
? ? ? 下面的問題是只能看到地址而不知道文件名和行號。至少有下面一些辦法
然后可以在gdb中使用info symbol 0x000xxxxx來定位到地址對應的函數名。這里的0x000xxxxx是ddms中地址把前三位變成0。因為gdb .so中使用.so的靜態地址,而ddms中的地址經過動態鏈接,是內存的虛擬地址。但動態鏈接并不改變地址的后五位,所以這里后五位保持不變,前三位變成0,從而轉換為so的靜態地址。
然后用info line xxx.cpp:xxx來定位具體行。
這個方法比較繁瑣,因為當時自己沒找到好辦法,就這么用的。
/other/dev分析和優化
自己用的是4.2版本的android。每次打開preference setting,/other/dev的private dirty都會增加很多(10M作用),并且不會釋放。通過查看smaps,發現是/dev/pvrsrvkm導致的(4.3后設備名改為kgsl-3d0)。這個是顯示相關的設備,按我的理解,大概是顯存(如果沒有獨立顯存,那是用于顯示的內存)。通過網上查詢,并不是只有我遇到這個問題。例如chrome也有這個問題。但還是不知道為何這個會增加。在一通亂試后,發現如果對activity設置android:hardwareAccelerated=false,就能解決。此時只增加shared dirty,并且關掉activity,內存會被釋放。后來再查,看到stackoverflow上這篇文章,才知道這是4.2的一個bug。4.3和4.1都沒有問題。
.Dex mmaps優化
這個是java代碼編譯只會的.dex文件的大小。
開始自己使用eclipse編譯出來的apk作性能分析,發現這個也有幾M。但release版本的卻不到1M。轉念一想,原來是proguard的作用。proguard是android自帶的混淆器,會對java的類名,函數名,變量名等重新命名,給一個非常短的名字。有兩個作用,一個是使得反編譯的代碼不容易理解,另一個就是減少了dex文件的大小。經過這次內存分析,才發現其效果還是非常明顯的。
因為proguard無法對res下的layout,xml文件做混淆,所以他們引用到的java類(例如一些view類)的名字是不能被改變的。所以一個小經驗是讓xml文件盡量少的引用java類,從而提高混淆的比例。
總結
關于android內存優化,自己就先做了這些。整體思路就是從宏觀到微觀,利用各種工具和網絡資料,從內存占用量最多的模塊下手,一步步的分析原因,解決問題。再細化下去,還有很多代碼級別的優化,例如perf tips里介紹了很多經驗,memory efficient java也很值得參考。有時間再在這個級別做更多的優化。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀
總結
以上是生活随笔為你收集整理的Android内存分析和调优的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 算法设计方法
- 下一篇: Java中Set巧用,去掉重复数据