本文主要有以下三部分內容:
第一部分:簡單介紹開發者指南上內存相關的文章。
第二部分:總結移動App性能評測與優化內存篇相關內容。
第三部分:Android內存相關好文章
開發者指南內存篇
以下是官方文檔內存篇相關內容:
管理應用內存
內存管理預覽
調查 RAM 使用情況
使用 Memory Profiler 查看 Java 堆和內存分配
dumpsys meminfo
管理應用內存
主要內容有:
(1)監控可用內存及內存使用:在手機有內存壓力時,系統會發廣播進行提示,應用根據這些
信息對內存使用作出恰當的處理,調用getMemoryInfo()方法去查詢當前設備的可用內存堆內存空間。
(2)從代碼角度優化內存:節省使用Service,除非sercie要去執行一個任務,否則不應該一直在后臺駐留,
使用Android框架提供的優化過的數據結構,如ArrayMap替代Haskmap等等;代碼抽象會
代碼嚴重的開銷,所以盡量少使用代碼抽象,使用nano protobufs進行序列化,避免內存抖動,因為內存抖動會
觸發更多的GC,影響手機性能,
(3)移除內存敏感的資源和庫:減少APK大小,在需要使用注解時,考慮使用Dragger,Dragger不會增加不必要內存使用。
謹慎使用外部庫,我們可能只需要外部庫一個很小的功能,如果引入外部庫可能會帶來更大的內存開銷。
內存管理預覽
主要內容有:
(1)垃圾回收:垃圾回收的兩個目標,首先是找到不再使用的對象,釋放不再使用的對象,
Android里面是一個三級Generation的內存模型,不同的Generation采用不同的垃圾回收方式,GC所占用的時間和它是哪一個Generation也有關系。
(2)共享內存:不同進程之間可以共享框架代碼和系統資源,應用和屏幕合成者通過匿名共享內存共享surface數據。
通過共享內存可以節省內存資源。
(3)分配和釋放內存:應用內存可以根據自己的需求不變變大,但是不能超多每個應用的最大限制。
(4)限制應用內存:如果應用申請的內存超過自己最大可使用內存,這時候就會有內存溢出,應用可以通過getMemoryClass()
方法獲取應用最大可使用堆的大小。
(5)切換應用:當應用在前后臺切換的時候,如果遇到內存緊張,系統會根據LRU緩存去殺掉部分進程,應用在后臺時使用的
內存越小,就越不容易被殺掉,這樣應用在切換到前臺時更快。
調查 RAM 使用情況
即使您在開發過程中遵循了管理應用的內存的所有最佳做法,您仍然可能泄漏對象或引入其他內存錯誤。唯一能夠確定您的應用盡可能少地使用內存的方法是,利用本文介紹的工具分析應用的內存使用情況。主要內容有:
(1)解讀Dalvik、ART虛擬機GC日志,主要有GC原因、垃圾回收名稱、釋放大小,暫停時間等等;
(2)捕捉堆轉儲:堆轉儲是應用堆中所有對象的快照。堆轉儲以一種名稱為 HPROF 的二進制格式存儲,您可以將其上傳到分析工具中。
應用的堆轉儲包含應用堆整體狀態的相關信息,以便您能夠跟蹤在查看堆更新時發現的問題。
(3)查看堆更新:使用 Android Monitor 在您與應用交互時查看應用堆的實時更新。實時更新提供了為不同應用操作分配的內存量的相關信息。
您可以利用此信息確定是否任何操作占用了過多內存以及是否需要調整以減少占用的內存量。
(4)分析堆轉儲:堆轉儲使用與 Java HPROF 工具中類似但不相同的格式提供。Android 堆轉儲的主要區別是在 Zygote 進程中進行了大量的分配。
因為 Zygote 分配在所有應用進程之間分享,所以它們對您自己的堆分析影響不太大。
(5)跟蹤內存分配:具體內容可參考:使用 Memory Profiler 查看 Java 堆和內存分配,Android Profiler是測量應用性能好工具主要有以下內容:使用 CPU Profiler 檢查 CPU Activity 和函數跟蹤
使用 Memory Profiler 查看 Java 堆和內存分配
利用 Network Profiler 檢查網絡流量
(6)查看整體內存分配:使用 adb shell dumpsys meminfo <package_name|pid> [-d] 命令觀察應用內存在不同類型的 RAM 分配之間的劃分情況,-d 標志會打印與 Dalvik 和 ART 內存使用情況相關的更多信息,輸出列出了應用的所有當前分配,單位為千字節。我們應該熟悉不同內存類型的分配,詳細內容請閱讀官方文檔。
(7)觸發內存泄漏:內存泄露越小,就需要運行更長的時間發現泄露點,可以通過橫豎屏切換,應用切換來觸發內存泄露。
使用 Memory Profiler 查看 Java 堆和內存分配
主要內容有:為什么分析應用內存、Memory Profiler概覽、如何計算內存、查看內存分配、捕獲堆轉儲、將對轉儲另存為Hprof、分析內存技巧等,具體內容請直接參閱官方文檔。
dumpsys meminfo
dumpsys meminfo具體內容請閱讀 dumpsys,該內容和調查 RAM 使用情況中的查看整體內存分配部分基本一致。
以上就是官方文檔關于內存部分的簡單介紹,更具體的內容請閱讀官方文檔。
移動App性能評測與優化內存篇相關內容
主要將自己認為重要的內容記錄下來,部分內容會根據最新的Android版本加入一些自己的理解。雖然Android一直在不停的演進,書中描述的部分問題在最新Android手機上已經不存在,但是書中描述的一些分析方法和經驗還是很值得學習的。
MAT工具使用技巧
MAT打開hprof文件之后,使用Top Consumers和Component Report功能,使用這些功能能快速定位大塊內存消耗,由于虛擬機不會區分系統資料和應用自身的對象,可以采用兩種方式來區分系統框架資源和應用自身對象
方法一:hprof-conv轉換時添加“-z”參數;
方法二:hprof已經轉換過了,在數據中尋找應用的Application類對象,可以使用OQL語句查詢應用自身對象;
使用-z和OQL查詢語句得到的對象集合就是應用代碼分配的部分。這樣就剔除了系統資源的影響。
Dilvik Heap常見問題及相關分析工具
(1)功能反復執行,Heap一直在持續增長,這種情況通常出現內存泄露,適合用LeakCanary等泄露工具進行白盒測試分析;
(2)代碼執行時出現頻繁的GC,Heap Alloc內存大幅波動,通常是分配了許多臨時變量和數組,雖然又被回收,適合使用Heap Viewer/Allocation Tracker等工具來查看具體分配的對象;
(3)每次啟動應用之后,Heap內存相對以前版本穩定增長,可能是由于新功能機代碼改動引入的固定內存增長,獲取Heap Dump進行多版本使用前后對比來查找增長原因。
(4)Heap Alloc變化不大,但進程Dalvik Heap Pss內存明顯增加,是由于分配了大量小對象造成內存碎片的原因。
新問題
新功能可能會分配幾萬到幾十萬字節的內存,實際增加的內存為2MB,但是Dalvik heap內存并沒有增加太多(200kb),說明問題不在Dalvik里就能解決,需要我們進一步深挖,Heap內存并不是應用的全部,可以通過dumpsys查看應用整個進程的使用量,以及各部分的使用量,最后發現Dalvik heap Pss部分增加比較多。最常用觀察進程內存的方法 adb shell dumpsys meminfo packge name| pid
上述描述的問題就是Dalvik heap pss內存增加了2M,Dalvik heap Alloc值增長了273kb,但Dalvik heap Free也能看出大部分增長的內存是處于空閑狀態的。各種怪異的問題,常用的方法找不出原因,說明有更深層次的原因,Java代碼的內存分配和釋放是由虛擬機管理的,我們需要通過虛擬機機制來探索內存增長的原因。
Dalvik heap內部機制
(1)為什么DVM占用內存不釋放,需要于都DVM內存分配代碼(位于dalvik/vm/alloc下),目前ART虛擬機已經取代了DVM,具體代碼位置沒有看過。
(2)新建對象之后,由于要向對應的地址寫入數據,內核開始真正分配該地址對應的4KB物理內存頁面。代碼在Alloc.cpp中。
(3)運行一段時間之后開始GC,GC時可能會進行trim,即將空閑的物理頁面釋放回系統,表現為private dirty/pss下降,相關代碼在HeapSource.cpp中。
問題所在以及優化
(1)在了解DVM分配和釋放內存的機制之后,根據dumpsys觀察到的現象,猜測可能是頁面利用率的問題,如在GC之后,大部分對象被釋放,少部分留下來,導致整頁的4KB內存可能只有一個小對象,但統計的時候是按4KB來計算。
(2)將MAT中的數據導出為csv格式,然后按也頁面進行統計,可以查看也頁面利用率統計結果圖,利用率低的頁面增加說明小對象碎片數量增加。
(3)取出步驟2中使用不滿2KB的頁面的內存塊地址,重新導入MAT得到對象列表,基本可以看出那些對象造成了內存的碎片化。
(4)問題基本過程還原:生成對象過程需要很多臨時變量,批量生成過程中還有空閑內存,虛擬機沒有垃圾回收,完成后進行垃圾回收,清楚了所有的臨時變量,留下碎片化內存,造成碎片化類似代碼如下:
private Object result[] = new Object[NUM];
private Object test[] = new Object[NUM];
void test(){
for(int i = 0; i <NUM ; i ++ ){
byte[] tmp = new byte[NUM];
result[i] = new byte[NUM];
test[i] = new byte[NUM];
}
}
執行以上代碼之后通過MAT查看數組每個成員的內存地址,發現都是不連續的,這就到消耗很多的物理頁面,增加Heap Free。造成例子中的問題(書中沒有描述具體如何操作,我自己也沒有嘗試,感興趣的可以試驗一下,但是該問題在Android使用ART虛擬機之后就不會存在碎片化問題)。
總結
(1)MAT是探索java堆并發現問題的好工具,能快速發現常見圖片和大數組問題,但是MAT不是萬能的,比如該問題隱藏在對象地址中。內存分配的最小單位是頁面,大小通常為4KB;盡量不要在循環中創建很多臨時變量,可以將大型循環拆開、分段、按需執行;
(2)在JVM中,虛擬機借助標記整理算法將散布的內存移動到一起,這樣就不存在頁面利用率的問題,但是在DVM由于使用Mark-Sweep標記清除算法,該算法不能移動對象,即沒有內存整理,這樣就導致了內存碎片問題,導致以上問題的產生。目前Android使用ART虛擬機取代DVM,ART使用了標記整理算法進行內存回收,所以使用ART虛擬機的Android系統就不存在內存碎片問題,DVM與ART虛擬機區別可參考JVM、DVM以及ART虛擬機簡介
內存原理
內存除了Dalvik Heap pss以外還有其它許多消耗內存的部分,對Dalvik heap pss優化后,可能會發現Delvik other和Mmap在內存中的比重加大,我們需要繼續尋找辦法對在其他部分內存進行優化,由于對這部分不熟悉,我們需要先去了解背后的原理,才能有針對性的去研究如何優化這部分內存。
從物理內存到應用,我們首先要了解系統的內存機制,搞清楚屋里內存如何被分配到各個進程,以及共享內存的機制,這些機制對內存優化有很大的幫助,根據Google提供的Android架構圖可以看到Android是基于Linux內核的,因此底層內存分配和共享機制與Linux基本相同,由于Android是為移動設備設計的,Android擴充了許多內核機制和實現。對內存影響較大的是Ashmem和Binder機制,在Ashmem和COW機制基礎上,Android進程最明顯的內存特征是與zygote共享內存,為了加快啟動速度及節約內存,Android應用進程都是由zygote fork出來,由于zygote已經載入完成的Dalvik虛擬機和Android應用框架的代碼,fork出來的進程和zygote共享同一塊內存,這樣就節約了每個進程單獨載入的時間和內存,應用進程只需要載人自己的Dalvik字節碼及資料就可以運行。
一個運行的Android應用進程會包含以下幾個部分:
(1)Dalvik虛擬機代碼(共享內存)
(2)應用框架代碼(共享內存)
(3)應用框架資源(共享內存)
(4)應用框架so庫(共享內存)
(5)應用的代碼(私有內存)
(6)應用的資源(私有內存)
(7)應用的so庫(私有內存)
(8)堆內存、其它部分(共享/私有)。
通過dumpsys meminfo可以觀察內存值,它將不同額內存消耗分類統計,通過閱讀和分析dumpsys meminfo的代碼(自己未閱讀),可以了解Android是如何劃分各部分內存的,知道dumpsys是如何統計各部分內存的。
Android底層預計Linux內核,進程內存信息和Linux一致,Dalvik heap之外的信息都能夠從/proc/pid/smaps/中獲取。我們可以通過 adb shell cat /proc/pid/smaps > smapsinfo.txt將smaps詳細信息重定向到文本文件中進行查看。smaps中信息如下:
(1)/dev/ashmem/dalvik-heap和/dev/ashmem/dalvik-zygote歸為Dalvik-heap;
(2)其它以/dev/ashmem/dilvik-開頭的內存區域歸為Dalvik-other
(3)文件的mmap按已知的幾個擴展名分類
(4)其余歸為Other mmap;
由于Android已使用ART虛擬機代替Dalvik虛擬機,暫時不知道最新的smaps信息如何對內存進行分類。
zygote內存共享機制
Pss進程實際使用的物理內存,是私有內存加上按比例分配的各進程共享內存得到的值,共享內存是zygote加載的Android框架部分,會被所有的進程分享,Dalvik pps內存=私有內存+共享內存/共享進程數,所以當一個進程結束后,它所占用的共享內存就會被其它使用,該共享庫的進程所分擔,所以一個進程結束,可能會導致其它進程的Pss內存增加。
優化Dex相關內存
隨著代碼功能的增加,代碼復雜度也在不斷變大,這時候會發現Dalvik heap和Dex mmap這兩部分消耗的內存增大(占總內存比例變大),Dalvik other存放的是類的數據結構及關系,Dex mmap是類函數代碼和常量,通常優化這部分內存,需要從代碼出發,但如果我們深入理解系統,也能夠找到其它方法來降低這部分的內存消耗,所以我們在優化內存時,不應該只優化堆內存,在我們搞定其它類型內存的含義以及原理之后,也是能夠對其它部分的內存進行優化。雖然最新的Android版本Dalvik Other占用的內存雖然不大,但是這給優化內存提供了一種思路。簡單一段代碼在一個空應用執行以后,可以看到對應heap、other、dex mmap的內存增長,heap增長可以通過代碼邏輯分析出來這段代碼需要分配多少,也可以在mat中看到新建對象消耗的內存,當應用使用完新創建的對象后,就會將heap內存釋放,但是other和dex mmap不會被釋放。
一個類的內存消耗以及new一個對象的步驟
虛擬機在執行這步時會做什么那?
第一步是loadClass操作,將類信息從dex文件加載到內存中;
(1)讀取.dex mmap中的class對應的數據;
(2)分配native-heap和dalvik-heap內存創建class對象;
(3)分配dalvik-linearAlloc存放class數據;
(4)分配dalvik-aux-structure存放class數據;
第二步:new instance操作,創建對象實例:
(1)執行dex mmap中的<clinit>和<init>代碼;
(2)分配dalvik-heap創建class對象實例;
如果對象引用了其它類型,那還需要先按照同樣的邏輯創建被引用的class,在創建一個類實例的每一步都需要消耗內存,可以大概計算一下new操作需要消耗的內存;根據虛擬機的代碼能夠得知class根據類成員和函數數目分配linearAlloc和aux-structure的多少,以及class本身及函數需要的字節數,我們再根據所以class總量進行平均計算得到一組數據:
第一步是loadClass操作,加載類信息;
(1).dex mmap:載入一個類需要先讀取259字節的mmap
(2)dalvik-linearAlloc:在linearAlloc區域分配437字節,存放類的靜態數據;
(3)dalvik-aux-structure:在aux區域分配88字節,存放各種指針
第二步:new instance操作,創建對象實例:
(1)dex mmap :為了執行類的構造函數,還需要讀取252字節mmap
(2)dalvik-heap:根據類的具體內容而變化。
由于內存最小分配單位是頁面,同時內存分配并不是連續分布,所以可能需要分配多個4KB頁面。
Dex mmap在Android應用中作用是映射class.dex文件,Dilvik虛擬機需要從dex文件中加載類信息、字符串常量,需要在調用函數時直接從mmap內存中讀取函數代碼來執行,所以該部分內存是程序運行必不可少的。
.dex文件將所有的class里邊所包含的信息全部整合在一個。可以使用Android SDK提供的dexdump工具來觀察dex文件內容,假設代碼里用到A1類后,還用到B1、C1、D1類,如果能在dex文件中將A1、B1、C1、D1類放在一起,虛擬機就只需要加載一個4KB的頁面,可以減少內存使用。優化思路就是調整dex文件中數據的順序,盡量將使用到的數據內容排列在一起。Proguard工具能夠對類名進行修改,根據程序運行的邏輯將那些會互相調用的類改為同一個packag名,這樣就可以使他們的數據排列在一起。
小結
(1)優化內存時,不只有堆內存,還有其它許多類型的內存能夠進行分析和優化;
(2)dex文件有很多優化空間,調整dex文件順序,可以節約mmap的內存;
(3)引入sdk和調用新的系統API需要考慮成本,不成用的功能可能導致大量的內存消耗,這時可以考慮多進程方案,將影響內存的操作放入到臨時進程執行。
總結
內存主要組成:
(1)native heap:Native代碼分配的內存,虛擬機和Android框架本身也會分配;
(2)Dalvik heap:Java代碼分配的對象;
(3)Dalvik Other:類的數據結構和索引;
(4)so mmap:Native代碼和常量
(5)dex mmap:Java代碼和常量;
內存工具:
(1)Android Studio/Memory Monitor:觀察Dalvik內存;
(2)dumpsys meminfo:觀察整體內存;
(3)smaps:整體內存的詳細組成;
(4)MAT:分析Java對并發現問題好工具;
經驗總結:
(1)內存分配的最小單位是頁面,通常為4KB;
(2)碎片不僅僅是Dalvik內存,還包括各種mmap可能產生的內存碎片,在Android4.4引入ART虛擬機之后,ART虛擬機的垃圾收集器(MarkSweep+Semispace)會進行碎片整理,所以就不存在碎片問題;
性能優化:
(1)盡量不要在循環中創建很多臨時變量,可能會觸發頻繁的GC,導致內存抖動;
(2)性能優化不只有堆內存優化,其它類型的內存,我們需要先了解其原理之后,就可以有針對性進行分析和優化;
Android內存其它好文章
Android性能優化-內存泄漏(上)
Android性能優化-內存泄漏(下)
leakcanary源碼學習隨筆
Android 性能優化的方方面面都在這兒-內存優化
Android性能優化-方法區導致內存問題實例分析