1.1 內存分析常用工具
1.Android studio 自帶memery Profile 進行內存觀察,這樣可以看到內存的消耗量,如果存在明顯的內存泄漏,隨著功能的反復使用,內存會出現不斷升高,即使出現GC也沒法降下來.可以進行錄制,保存hprof文件.
2.使用SDK自帶的Android Device Monitor,觀察Heap堆的內存變化,配合Cause GC 按鈕進行觀察頁面的內存動態(tài)值,遇到可疑的內存泄漏進行保存hprof .
3.收集到內存數據采用eclipse Memory Analyzer Tool (MAT)來分析.
使用MAT分析的一些技巧:
通常先使用Android SDK 提供的hprof-conv 轉換工具,把hprof 轉換成為去除系統(tǒng)內存使用的內存,而只留下應用代碼分配的內存數據來分析.(hprof-conv 工具在Android SDK/platformtools/)
hprof-conv -z <infile><outfile>可以使用OQL進行篩選 : select * from instanceof java.long.Object s where s.@objectAddress>1107296256
1.2規(guī)范測試流程以及常見問題
1.2.1測試流程
1.代碼:
盡量保證內存測試代碼是純凈版本,不應該附加多余的Log和調試組件的,把測試的代碼全部關閉.因為這些代碼可能會臨時分配內存,引起更多的GC,導致應用出現運行緩慢,卡頓現象.
2.測試場景:
測試場景有兩類:1)屬于新增功能或改動某項功能,需要對該功能進行性能測試.
2)整體性能測試.
各類場景測試的 重點:
包含圖片顯示的界面
網絡傳輸的大量數據
需要緩存數據的場景
場景轉換成用例
多張圖片的前臺進程
多個場景來回切換
長時間運行進程的內存增長
1.1.2 Dalvik Heap的常見問題
常見問題一般有下面幾種:
隨著功能反復的執(zhí)行,heap內存一直在持續(xù)增長.這種情況一般出現內存泄漏,此時最合適使用LeakCanary 等工具進行白盒測試kk
代碼執(zhí)行是出現頻繁的GC,Heap Alloc 內存大幅度波動.這種情況通常是分配了許多臨時變量或數組,隨后有迅速回收.確定具體的 場景后可以使用Heap Viewer/Allocation Tracker查看具體分配情況.
每次啟動應用,Heap內存相比以前版本穩(wěn)定增長.一般由于App待機或者使用某項功能后,可能是由于新功能引入的固定內存增長. 可以采用HeapDump 后對比多版本或功能的前后對比,找到原因.
Heap Alloc變化不大,但是進程Dalvik Heap Pss(Proportional Set Size)內存明顯增加.是由于分配了大量小對象找出內存碎片.
1.2.4新的問題特殊的情況
注意,Heap 內存并不是應用的全部,我們在設置或者其他管理工具看到的內存大小是整個應用的全部進行內存使用量.有可能是出現在Heap部分完全沒有增長,而其他部分有增長情況.
adb shell dumpsys meminfo com.xiu.app
Andorid studio 工具查看的內存是HeapAlloc的大小,而從上圖可以看出 HeapAlloc 和Dalvik Heap大小不一樣,除了DalvikHeap 之外還有其他部分會消耗內存.
可以進行對各個部分的PPS 進行分析.
1.2.5 進一步挖掘Dalvik Heap內存
對于簡單的問題無法回答內存遞增的情況下,例如
“這個版本引入了一個簡單的庫,內存就漲了20M”
“這些代碼只是初始化了幾個對象,還沒開始用呢”
“一行代碼都沒改,怎么會漲呢?”
通過分析,無法正常解答時需要更深入底層DVM (Dalvik Virtual Memory)機制
1.2.6 Dalvik Heap 內存機制
Dalvik 內存分配源碼 dalvik/vm/alloc ,具體的內部機制:
DVM使用mmap 系統(tǒng)調用從系統(tǒng)分配大塊內存作為java heap.根據機制,如果分類的內存尚未真正使用,就不計入privateDirty 和pss.一般處于shareDirty共享內存.
新鍵對象后,由于需要向地址寫數據,一般是分配一個4KB的物理內存頁面
在運行過程中會執(zhí)行GC,有些可以回收,有些一直存在
在GC 過程中有可能會觸發(fā)TrimHeap,即釋放空閑內存表現為PrivateDirty/Pss下降.
頁面利用率:
頁面利用率問題?(碎片問題)
由于內存分配的時候,開始會分配的比較滿,經過GC后,大部分內存釋放,但是有少部分留下來了.在這種情況下會產生問題,整頁的4KB內存中可能只有一個小對象,但是在統(tǒng)計PrivateDirty/Pss是還是按4KB計算.主要原因是DVM在進行內存回收的時候是采用Mark-Sweep 方式而不是Compating GC,并沒有對對象進行移動,造成內存碎片(內存空洞).
分析:
用MAT 導出csv 格式.
可以把對象按照內存地址(取高位地址&0xfffff000)分布,相同高位的對象處于相同的頁面.可以得出柱狀分布圖.通過對比觀察,如果發(fā)現 柱狀短的數量多(說明頁面利用率低的很多),這樣就說明小對象碎片比較嚴重.
經驗總結:
內存分配的最小單位是頁面,通常4KB
對于開發(fā)人員,盡量不要在循環(huán)中創(chuàng)建很多臨時變量.
可以將大型的循環(huán)拆散,分段或者按需執(zhí)行.
1.4 進階 內存原理
對于緩存的策略,不能簡單粗暴的將所有的緩存一次生成,這樣會導致大量的碎片,需要一種合適的策略來生成.
使用adb shell dumpsys meminfo com.xiu.app
總結規(guī)律如下:
通常大致了解到.Dalvik Other 和Mmap 和代碼數量有關,對于越復雜的應用,這部分內存就越多.
現在需要了解背后的原理.
1.4.1 從物理內存到應用
由于linux的的底層內存分配和共享機制,android 也是符合這一原則.在Ashmem以及COW(Copy -On-Write)的機制基礎上,Android進程最明顯的內存特征是與zygote共享內存.因為Android進程是用zygote fork出來的,所以Android的虛擬機和Zygote進行共享,應用只需要載入自己的Dalvik字節(jié)碼及資源既可以開始工作.
綜上所述,一個在運行Android應用進程會包含以下幾部分:
Dalvik虛擬機代碼(共享內存)
應用框架的代碼(共享內存)
應用框架的資源(共享內存)
應用框架的so庫(共享內存)
應用代碼(私有內存)
應用的資源(私有內存)
應用的so庫(私有內存)
堆內存,其他部分(共享/私有內存)
下面是dumpsys meminfo工具的詳細統(tǒng)計各部分內存值:
1.4.3 smaps
由于android 是基于linux 內核,進程內存信息和linux是一致,所以Dalvik heap之外的信息都可以在/proc/<pid>/smaps中取得.
用ps 命令可以查看 linux 系統(tǒng)下所有進程信息.
Annroid 中ps命令參數:-t -x,-p,-P,-c [pid|name]-t顯示進程下的線程列表-x 顯示進程耗費的用戶時間和系統(tǒng)時間,單位s-P 顯示調度策略叛赚,通過是bg or fg 匪煌,當獲取失敗將會un和er比之前打印的內容多出了一列PCY,表示進程的調度等級
PID:進程號 PPID:父進程號 VSIZE :進程的虛擬內存大小 RSS :進程分配到的屋里內存大小 WCHAN:程正在睡眠的內核函數名稱裆悄;該函數的名稱是從/root/system.map文件中獲得的 NAME :進程名
通過dumpsys meminfo com.xiu.app
dumpsys 統(tǒng)計的各個內存塊的pss,shared_dirty,private_dirty,按照以下的原則進行歸類:
/dev/ashmem/dalvik-heap 和/dev/ashmem/dalvik-zygote ==>Dalvik Heap
其他以/dev/ashmem/dalvik- 開頭的區(qū)域 ==> Dalvik Other
/dev/Ashmem 下所有不以dalvik- 開頭的區(qū)域 ==>Ashmem
/dev下的其他內存區(qū)域 ==>Other dev
,如[stack],[malloc],Unknown等 ==>其他部分
Dalvik :
在上述我們直到Dalvik 包括Dalvik heap,和Dalvik other.
Dalvik heap : Dalvik-heap 和Dalvik-zygote .堆內存,所有的java對象實例都放在這里.
Dalvik Other :
*LinearAlloc
*Accounting
*Code_Cache
1.LinearAlloc 包括 dalvik-LinearAlloc.線性分配器,虛擬機存放載入類的函數信息,隨著dex數量增加而增加,注明的 65535個函數的限制就是從這里來的.
- Accounting 包括 /dev/ashmem/dalvik-aux-structure,/dev/ashmem/dalvik-bitmap-2,/dev/ashmem/dalvik-card-table .這些主要是作為標記和指針表使用. Dalvik-aux-structure隨類及方法數增大而增加,dalvik-bitmap 隨著dalvik-heap 增大而增大.
3.Code_cache 包括/dev/ashmem/dalvik-jit-code-cache .jit編譯代碼后的緩存,隨著代碼復雜度增加而變大.
因為堆內部的內存分配往往是應用消耗內存最多的地方,所以有效的方法就是減少Dalvik-heap中的創(chuàng)建對象,減少代碼量.
由于這部分的內存增長取決與代碼復雜度,因此通常情況下沒有簡單直接的方法降低他們內存的消耗.
mmap
系統(tǒng)會將一些文件mmap到內存中,對各個文件進行mmap的時機以大小比較復雜,dex_mmap 是主要的內容.
1.4.3 zygote 共享內存機制
對于meminfo 輸出的Heap Size/alloc /Free 部分的數值,這些數值是Dalvik 虛擬機統(tǒng)計的內存堆的使用量,但是這些值是如何對應到Pss內存中,以及Heap Alloc 和Heap Pss 相差不遠,但是又不一樣.他們之間的關系如下:
Heap Alloc統(tǒng)計是由虛擬機分配的所有應用實例的內存,所以會把應用從zygote共享的部分也算進去,于是Heap Alloc的值總是比實際物理內存使用值要大.
Pss表示進程的實際使用物理內存.是有私有內存+共享內存的按比例分到的值.
Dalvik Pss=Private dirty+(Shared Dirty/共享內存進程數)
1.4.4 多進程應用
當一個進程結束后他所占用的共享庫內存將會被其他仍然使用的進程所分擔,共享庫消耗的物理內存并不會減少.實際上對所有共享使用了這個庫的應用,Pss都會增加,因為分擔的共享內存增加了.對于一般的應用只共享了zygote 進程的Android框架等基礎部分,通常手機使用的進程樹達到幾十 至上百個.所以某個進程結束后,其他進程內存增加的情況并不明顯.
但是對于多進程的應用來說,由于多進程之間會共享很多內容,包括代碼,資源,so庫 等,因此單個進程結束后對其他進程影響比較明顯.
由此可見,在統(tǒng)計多進程應用內存優(yōu)化時候需要綜合考慮,以免優(yōu)化了一個進程內存,卻造成其他進程的內存增長.
1.5 案例: 優(yōu)化dex 相關內存
Dalvik Mmap 和Dalvik Other會隨著代碼復雜度增加而增大.而這兩部分的內存將接近總內存的一半.在對Dalvik Heap做了優(yōu)化之后,繼續(xù)研究這部分內存有何優(yōu)化.
Dalvik Other存放的是類的數據結構和關系.而Dalvik mmap是類的函數代碼和常亮.通常情況下,減少這部分內存,需要精簡代碼,精簡無用代碼,或者將功能插件化.如果深入理解系統(tǒng)還有其他的方法降低這部分內存消耗.
1.5.1 從class 對象說起
1.5.2一個類的內存消耗
首先在代碼中使用一個類,例如一下代碼:
Foo f=new Foo();
虛擬機在執(zhí)行到這步時會做什么呢?
第一步:loadClass ,將類信息從dex文件加載內存:
1)讀取.dex mmap中class對應的數據
2)分配Native-heap和dalvik-heap內存創(chuàng)建class對象
3)分配dalvik-LinearAlloc存放class數據
4)分配dalvik-aux-structure存放class數據
第二步 new instance ,創(chuàng)建對象實例
1)執(zhí)行 .dex mmap中<clinit> 和<init>代碼
2)分配dalvik-heap 創(chuàng)建class實例
計算下new 操作需要消耗的內存:
1.5.3 dex mmap
dex mmap 在android的作用是映射classes.dex文件Dalvik虛擬機需要從dex文件中加載類信息\字符串常量,還需要在調用函數時候直接從mmap內存中讀取函數代碼來執(zhí)行,所以該部分內存是程序運行必不可少的.
在一個實例,可以發(fā)現dex 的文件排列順序導致內存有可能被浪費.原因是因為dex文件在生成是是按字母順序排列的,由于4KB頁面加載的原因,實際運行是會加載許多相鄰的但不會被用到的數據.例如在代碼中用了A1類,虛擬機需要加載包含A1類的數據頁面,但由于A1只有1KB,那么加載的4KB頁面中還會有A2,A3,A4類,總共占4KB內存.
假設我們的代碼中在用到A1類后,還會用到B1,C1,D1 類,那么如果能在dex文件中將A1,B1,C1,D1的類放在一起,虛擬機只需要加載 一個4kB頁面,不僅減少了內存使用,對程序運行的速度也有好處.因此調整的思路就是優(yōu)化dex文件的數據的順序,將能夠用到的數據緊密排列在一起.