Android記憶體分析和調優(上)

yangxi_001發表於2013-12-05
最近我們的android app佔用了大量記憶體,於是領導安排做減少記憶體佔用的工作。
要優化記憶體,首先要做的就是分析記憶體佔用情況。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,下次寫如何具體分析這五項。

相關文章