Android分配個(gè)應(yīng)用的大小是有限制,且在設(shè)備出廠之后已經(jīng)確定,單個(gè)應(yīng)用可用的最大內(nèi)存的配置位于/system/build.prop文件中的dalvik.vm.heapgrowthlimit配置項(xiàng)。
雖然Android使用的JVM具有內(nèi)存管理(自動(dòng)回收)的能力,但是對(duì)內(nèi)存使用不當(dāng)會(huì)導(dǎo)致應(yīng)用出現(xiàn)異常,包括常見的OOM、內(nèi)存泄漏、內(nèi)存抖動(dòng)等引發(fā)的崩潰、卡頓等現(xiàn)象。我們一般主要針對(duì)這三種內(nèi)存問題進(jìn)行優(yōu)化處理:
- OOM
Out of memory, 內(nèi)存溢出,當(dāng)應(yīng)用申請(qǐng)內(nèi)存發(fā)現(xiàn)超出了JVM的最大限制時(shí)候,就會(huì)拋出內(nèi)存溢出異常,引發(fā)程序崩潰。引發(fā)OOM常見原因有:內(nèi)存泄漏的累積導(dǎo)致無法申請(qǐng)更多內(nèi)存、創(chuàng)建大內(nèi)存對(duì)象(如大容量數(shù)組、載入大的文件、載入大的圖片等)。 - 內(nèi)存泄漏
一個(gè)對(duì)象的超出了其生命周期,導(dǎo)致JVM無法回收。這樣無法回收的對(duì)象堆積多了會(huì)導(dǎo)致應(yīng)用可能無法申請(qǐng)到內(nèi)存進(jìn)而導(dǎo)致OOM。常見的內(nèi)存泄漏有:- 單例持引起的內(nèi)存泄漏,如單例持有activity、context、view、drawabl等
- 靜態(tài)變量引起的內(nèi)存泄漏,如靜態(tài)變量持有activity、context、view、drawabl等
- 非靜態(tài)內(nèi)部類引起的內(nèi)存泄漏,原因:非靜態(tài)內(nèi)部類會(huì)隱式持有外部類實(shí)例
- 匿名內(nèi)部類引起的內(nèi)存泄漏,如handler、線程匿名內(nèi)部類runnable、callback等
- 資源未釋放引起的內(nèi)存泄漏,如讀寫文件沒有關(guān)閉、網(wǎng)絡(luò)流操作沒有關(guān)閉、Bitmap沒有釋放等
- 廣播沒有及時(shí)取消注冊(cè)
- 內(nèi)存抖動(dòng)
內(nèi)存抖動(dòng)是因?yàn)樵陬l繁的創(chuàng)建、回收對(duì)象,引發(fā)的頻繁GC,進(jìn)而影響主線程,最終導(dǎo)致卡頓現(xiàn)象。
要知道怎么正確的使用內(nèi)存,首先需要了解java虛擬機(jī)(即JVM)的內(nèi)存管理機(jī)制。
JVM內(nèi)存管理機(jī)制
JVM內(nèi)存管理機(jī)制是通過根搜索算法,在合適的時(shí)期檢索對(duì)象是否可達(dá),當(dāng)對(duì)象不可達(dá)時(shí)會(huì)被回收,如果對(duì)象可達(dá)則不會(huì)被回收。
對(duì)象的生命周期
一個(gè)對(duì)象從創(chuàng)建到銷毀回收是其生命周期的表現(xiàn),在開發(fā)階段我們是可以預(yù)測(cè)到對(duì)象的生命周期范圍,什么時(shí)候創(chuàng)建什么時(shí)候回收,如果沒有被正?;厥站蜁?huì)引發(fā)對(duì)象不可被回收導(dǎo)致的內(nèi)存泄漏。
哪些對(duì)象需要回收
使用根搜索算法GC Root Trace通過一系列名為GC Root的對(duì)象作為起點(diǎn),向下搜索,搜索所經(jīng)過的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到GC Root沒有應(yīng)用鏈相連,則表明此對(duì)象需要回收。以下是可以作為GC Root的對(duì)象:
- 全局靜態(tài)變量引用的對(duì)象
- 全局常量引用的對(duì)象
- 虛擬機(jī)棧幀中本地變量表中引用的對(duì)象
- 本地方法棧幀中本地變量表中引用的對(duì)象
什么時(shí)候回收內(nèi)存
介紹什么時(shí)候回收內(nèi)存前,先介紹下Android虛擬機(jī)的堆塊的管理情況,Android虛擬機(jī)遵循java虛擬機(jī)堆內(nèi)存分代管理的機(jī)制,主要?jiǎng)澐譃椋盒律?、老年代,其中新生代又分?個(gè)Eden(新生代)和2個(gè)survisor(Eden幸存的對(duì)象),eden、survisor內(nèi)存默認(rèn)按8:1:1,因?yàn)楹芏鄬?duì)象創(chuàng)建使用過后就會(huì)回收,真正存活下來的不會(huì)很多,所以給eden分配80%的占比可以有效提升內(nèi)存使用率,每次使用eden和1個(gè)survisor,回收時(shí)將存活的復(fù)制到另一個(gè)survisor中,然后清空eden和survisor,什么時(shí)候會(huì)回收內(nèi)存呢?一般是在各個(gè)內(nèi)存分代區(qū)內(nèi)存不足或者內(nèi)存快滿時(shí)會(huì)觸發(fā)內(nèi)存回收即GC。新生代觸發(fā)的是Minor GC,因?yàn)樾律蠖鄶?shù)是朝生夕滅,所以Minor GC比較頻繁,但速度會(huì)比較快;當(dāng)survisor中內(nèi)存不足或者存活年齡達(dá)到一定在就會(huì)將相應(yīng)的survisor中的對(duì)象復(fù)制到老年代中,老年代內(nèi)存回收是Major GC/Full GC,一般都會(huì)伴隨至少一次的Minor GC,Major GC速度相對(duì)比較慢,相比Minor GC可能會(huì)慢10倍。
怎么回收對(duì)象
新生代使用復(fù)制算法,將eden和1個(gè)survisor的內(nèi)存復(fù)制到另一個(gè)survisor中,接著清空原先的eden和survisor;老年代使用標(biāo)記—整理算法,即先標(biāo)記要回收的對(duì)象,再把存活的對(duì)象移到一段,接著就是清理掉端邊界以外的對(duì)象。
不論Minor GC還是Major GC在回收內(nèi)存的時(shí)候都會(huì)阻塞其它的工作線程,等完成GC之后再恢復(fù)工作線程。
內(nèi)存優(yōu)化
上面講述了虛擬機(jī)內(nèi)存管理機(jī)制,對(duì)應(yīng)內(nèi)存的優(yōu)化有以下建議:
防止內(nèi)存泄漏
- 避免全局靜態(tài)變量持有資源對(duì)象:如activity、非applicationcontext、fragment、view等
- 避免全局常量持有有資源對(duì)象:如activity、非applicationcontext、fragment、view等
- 避免單例持有資源對(duì)象:如activity、非applicationcontext、fragment、view等
- 對(duì)于內(nèi)部類要么使用靜態(tài)內(nèi)部類+弱引用,要么使用弱引用
- 對(duì)于匿名內(nèi)部類使用弱引用引用外部引用
- 資源使用完之后,及時(shí)釋放:文件io、cursor、網(wǎng)絡(luò)io用完之后及時(shí)釋放
防止內(nèi)存抖動(dòng)
- 避免創(chuàng)建大內(nèi)存對(duì)象,如:大內(nèi)存數(shù)組、加載大文件或者圖片
- 避免頻繁創(chuàng)建對(duì)象,如:避免在for語句中創(chuàng)建大量對(duì)象
- 需要頻繁使用的對(duì)象,可以通過緩存池復(fù)用,避免重復(fù)創(chuàng)建、釋放,在內(nèi)存緊張OnTrimMemory /OnLowMemory 時(shí)適當(dāng)釋放可以釋放的資源或者對(duì)象
- 使用圖片是可以時(shí)候565或者對(duì)圖片進(jìn)行裁剪、降低圖片質(zhì)量,也可以使用Glide,滑動(dòng)時(shí)暫停加載圖片,不滑動(dòng)時(shí)恢復(fù)加載圖片
- 字符串相加或者拼接通過StringBuilder替代,較少創(chuàng)建String對(duì)象節(jié)省內(nèi)存
- 使用SpareArray、ArrayMap替代HashMap
內(nèi)存分析
LeakCannary
項(xiàng)目中依賴LeakCannary庫,使用LeakCannary可以檢測(cè)內(nèi)存泄漏
Memory Profiler
使用Android Studio的內(nèi)存分析器可以對(duì)內(nèi)存分析,根據(jù)分析結(jié)果進(jìn)行相應(yīng)的優(yōu)化
如需打開內(nèi)存分析器,按以下步驟操作:
- 依次點(diǎn)擊View——》Tools windows——》Profiler(或者點(diǎn)擊工具欄中的Profiler圖標(biāo))
- 從Android Profiler工具欄中選擇要分析的設(shè)備和應(yīng)用進(jìn)程
- 點(diǎn)擊MEMORY時(shí)間軸上的任意位置打開內(nèi)存性能分析器
打開內(nèi)存性能分析器后,其界面如下圖所示:

1、強(qiáng)制執(zhí)行垃圾回收按鈕
2、選擇捕獲堆轉(zhuǎn)存heap dump的按鈕
3、暫停/跳轉(zhuǎn)到實(shí)時(shí)內(nèi)存數(shù)據(jù)的按鈕
4、事件時(shí)間軸:顯示應(yīng)用活動(dòng)狀態(tài)、用戶輸入事件(如touch的down/press等)、屏幕旋轉(zhuǎn)事件等
5、內(nèi)存分類用量統(tǒng)計(jì)
- total: 總共分配的對(duì)象的內(nèi)存大小
- Java: java/kotlin代碼分配的對(duì)象的內(nèi)存大小
- Native: c/c++分配的對(duì)象的內(nèi)存大小
- Graphics: 圖片緩沖區(qū)隊(duì)列向屏幕顯示像素所使用的內(nèi)存大?。ㄟ@部分是CPU共享的內(nèi)存,而不是GPU)
- Stack: 應(yīng)用java和原生堆棧使用的內(nèi)存。
- Code: 應(yīng)用處理代碼和資源的內(nèi)存(包括處理dex字節(jié)碼、so庫、字體等)
- others: 應(yīng)用使用了系統(tǒng)不確定如何分類的內(nèi)存大小
- Allocated: 應(yīng)用分配的java/kotlin對(duì)象數(shù)(不含c/c++分配的對(duì)象數(shù))
6、以圖表、坐標(biāo)軸的方式顯示內(nèi)存分配情況,x坐標(biāo)顯示的是時(shí)間、y軸左側(cè)標(biāo)記部分代表內(nèi)存大小、y軸右側(cè)標(biāo)記部分代表分配對(duì)象數(shù)、圖表部分代表各個(gè)類別分配的對(duì)象的內(nèi)存大小
捕獲堆轉(zhuǎn)儲(chǔ)
選擇內(nèi)存分析器中的Capture heap dump,點(diǎn)擊下方的Record按鈕,就開始捕獲堆轉(zhuǎn)儲(chǔ)了,可以點(diǎn)擊stop結(jié)束捕獲,結(jié)束捕獲之后會(huì)自動(dòng)加載捕獲到堆轉(zhuǎn)儲(chǔ)。下圖是捕獲到heap dump之后,打開的界面

1、過濾器
這部分主要用于對(duì)heap dump的數(shù)據(jù)進(jìn)行過濾,過濾我們關(guān)注、需要分享的部分,包括
- 選擇需要檢查的堆類型:
- view all heap:檢查分配內(nèi)存的所有堆
- view app heap:默認(rèn),檢查應(yīng)用在使用時(shí)分配內(nèi)存的主堆
- view image heap: 系統(tǒng)啟動(dòng)映像,包括啟動(dòng)期間預(yù)加載的類
- view zygote heap: 檢查寫時(shí)復(fù)制堆,這部分是應(yīng)用通過zygote 創(chuàng)建啟動(dòng)進(jìn)程時(shí)的堆
我們應(yīng)用端一般主要分析view app heap進(jìn)行分析主堆,排在java層面的內(nèi)存問題
- 選擇如何安排分派
- Arrang by class:默認(rèn),根據(jù)類名稱對(duì)所有內(nèi)存分配進(jìn)行分組
- Arrang by package: 根據(jù)包名對(duì)所有內(nèi)存分配進(jìn)行分組
- Arrange by callstack: 根據(jù)調(diào)用堆棧對(duì)所有內(nèi)存分配進(jìn)行分組
一般采用采用Arrang by class過濾占用內(nèi)存占比比較高的類進(jìn)行分析,Arrang by package根據(jù)包名定位自己代碼、三方代碼的內(nèi)存問題
- 選擇顯示那些類型的數(shù)據(jù)
- Show all class: 默認(rèn),顯示所有的類
- Show activity/fragment Leak: 顯示發(fā)生內(nèi)存泄漏的activity/fragment
- Show project class: 進(jìn)顯示項(xiàng)目相關(guān)的類
- 輸入過濾:在輸入框中可以輸入類名/包名來快速定位到具體類/包名下類的內(nèi)存分配情況
2、統(tǒng)計(jì)信息
- classes: 類類型總數(shù),不是實(shí)例對(duì)象哦
- Leak:發(fā)生內(nèi)存泄漏的數(shù)量
- count: 總關(guān)創(chuàng)建的使用的實(shí)例對(duì)象數(shù)
- Native Size: 原生c/c++使用的內(nèi)存總量
- Shallow Size: java使用的內(nèi)存總量
- Retained Size: 還在使用保留的內(nèi)存總量
3、創(chuàng)建的對(duì)象數(shù)其分配內(nèi)存情況
這部分會(huì)列舉過濾之后的所有類名、分配的對(duì)象數(shù)及內(nèi)存使用情況,包括
- Class Name: 類名
- Allocations: 此類創(chuàng)建的實(shí)例對(duì)象數(shù)量
- Native Size: 此類總共使用的原生內(nèi)存總量(只有android7.0+設(shè)備才能看到)(單位字節(jié))
- Shallow Size: 此類使用的java內(nèi)存總量(單位字節(jié))
- Retained Size: 此類實(shí)例對(duì)象仍存活而保留的內(nèi)存總大?。▎挝蛔止?jié))
4、類實(shí)例對(duì)象列表及其實(shí)例對(duì)象的詳細(xì)信息
在3中點(diǎn)擊某一個(gè)類,會(huì)在下半部分顯示此類的所有實(shí)例對(duì)象的信息,如點(diǎn)擊圖中的bitmap。
這部分左側(cè)顯示類的實(shí)例對(duì)象列表:實(shí)例對(duì)象+地址;點(diǎn)擊某個(gè)實(shí)例會(huì)在右側(cè)顯示此實(shí)例內(nèi)存分配的詳細(xì)信息,包括:
-
Fields
實(shí)例對(duì)象每個(gè)字段信息,包括如下信息:- Instance 此字段的名稱及其類型,如果是基本數(shù)據(jù)類型和String會(huì)同時(shí)顯示此字段的當(dāng)前值
- Depth: 此字段字段可達(dá)的最短跳數(shù),表示的是任意一個(gè)GC Root到此字段的最短鏈路邊數(shù)
- Native Size: 原生內(nèi)存中此字段的內(nèi)存大?。ㄖ挥蠥ndroid7.0+上的設(shè)備才會(huì)看到此列)
- Shallow Size: Java 內(nèi)存中此字段的內(nèi)存大小
- Retained Size: 此字段目前還保留的內(nèi)存大小
-
References:
實(shí)例對(duì)象的引用鏈信息,References中包括如下信息:- Reference: 實(shí)例對(duì)象的引用鏈,可以依次點(diǎn)擊展開顯示此實(shí)例被哪些實(shí)例對(duì)象所引用,通過引用鏈可以最終追蹤到GC Root
- Depth: 此實(shí)例對(duì)象可達(dá)的最短跳數(shù),表示的是任意一個(gè)GC Root到此實(shí)例對(duì)象的最短鏈路邊數(shù)
- Native Size: 原生內(nèi)存中此實(shí)例對(duì)象的內(nèi)存大?。ㄖ挥蠥ndroid7.0+上的設(shè)備才會(huì)看到此列)
- Shallow Size: Java 內(nèi)存中此實(shí)例對(duì)象的內(nèi)存大小
- Retained Size: 此實(shí)例對(duì)象目前還保留的內(nèi)存大小
我們可以在Fields和References中分析,如果發(fā)現(xiàn)可以點(diǎn)如可能存在內(nèi)存泄漏等,可以右鍵選擇Go to Instance顯示其實(shí)例內(nèi)存數(shù)據(jù);或者選擇Jump to source進(jìn)入此實(shí)例對(duì)象所在的源碼片段。
一般我們使用內(nèi)存分析器對(duì)內(nèi)存進(jìn)行分析時(shí),注重點(diǎn)在于:
- 首先關(guān)注內(nèi)存占比比較高的類及其實(shí)例對(duì)象引用鏈情況,排查是否有內(nèi)存泄漏、是否有優(yōu)化空間
- 關(guān)注某些類的實(shí)例對(duì)象數(shù)量比較大情況,排查是存在大量創(chuàng)建短時(shí)間內(nèi)又銷毀引起內(nèi)存抖動(dòng),結(jié)合源碼分析是否有優(yōu)化空間,如使用緩存池等
- 關(guān)注activity、context、view、Drawable等對(duì)象及其引用鏈情況,排查這些是否存在內(nèi)存泄漏
- 關(guān)注項(xiàng)目相關(guān)類的內(nèi)存分配及其應(yīng)用鏈情況,排查是否存在內(nèi)存泄漏、使用不當(dāng)情況等