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)異常,包括常見(jiàn)的OOM、內(nèi)存泄漏译断、內(nèi)存抖動(dòng)等引發(fā)的崩潰、卡頓等現(xiàn)象或悲。我們一般主要針對(duì)這三種內(nèi)存問(wèn)題進(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常見(jiàn)原因有:內(nèi)存泄漏的累積導(dǎo)致無(wú)法申請(qǐng)更多內(nèi)存、創(chuàng)建大內(nèi)存對(duì)象(如大容量數(shù)組男公、載入大的文件荤堪、載入大的圖片等)。 - 內(nèi)存泄漏
一個(gè)對(duì)象的超出了其生命周期枢赔,導(dǎo)致JVM無(wú)法回收澄阳。這樣無(wú)法回收的對(duì)象堆積多了會(huì)導(dǎo)致應(yīng)用可能無(wú)法申請(qǐng)到內(nèi)存進(jìn)而導(dǎo)致OOM。常見(jiàn)的內(nèi)存泄漏有:- 單例持引起的內(nèi)存泄漏踏拜,如單例持有activity碎赢、context、view速梗、drawabl等
- 靜態(tài)變量引起的內(nèi)存泄漏揩抡,如靜態(tài)變量持有activity、context镀琉、view、drawabl等
- 非靜態(tài)內(nèi)部類(lèi)引起的內(nèi)存泄漏蕊唐,原因:非靜態(tài)內(nèi)部類(lèi)會(huì)隱式持有外部類(lèi)實(shí)例
- 匿名內(nèi)部類(lèi)引起的內(nèi)存泄漏屋摔,如handler、線程匿名內(nèi)部類(lèi)runnable替梨、callback等
- 資源未釋放引起的內(nèi)存泄漏钓试,如讀寫(xiě)文件沒(méi)有關(guān)閉、網(wǎng)絡(luò)流操作沒(méi)有關(guān)閉副瀑、Bitmap沒(méi)有釋放等
- 廣播沒(méi)有及時(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ī)制是通過(guò)根搜索算法材义,在合適的時(shí)期檢索對(duì)象是否可達(dá),當(dāng)對(duì)象不可達(dá)時(shí)會(huì)被回收嫁赏,如果對(duì)象可達(dá)則不會(huì)被回收其掂。
對(duì)象的生命周期
一個(gè)對(duì)象從創(chuàng)建到銷(xiāo)毀回收是其生命周期的表現(xiàn),在開(kāi)發(fā)階段我們是可以預(yù)測(cè)到對(duì)象的生命周期范圍潦蝇,什么時(shí)候創(chuàng)建什么時(shí)候回收款熬,如果沒(méi)有被正常回收就會(huì)引發(fā)對(duì)象不可被回收導(dǎo)致的內(nèi)存泄漏攘乒。
哪些對(duì)象需要回收
使用根搜索算法GC Root Trace通過(guò)一系列名為GC Root的對(duì)象作為起點(diǎn)贤牛,向下搜索,搜索所經(jīng)過(guò)的路徑稱(chēng)為引用鏈持灰,當(dāng)一個(gè)對(duì)象到GC Root沒(méi)有應(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)建使用過(guò)后就會(huì)回收,真正存活下來(lái)的不會(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)存快滿(mǎn)時(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)部類(lèi)要么使用靜態(tài)內(nèi)部類(lèi)+弱引用嚎花,要么使用弱引用
- 對(duì)于匿名內(nèi)部類(lè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語(yǔ)句中創(chuàng)建大量對(duì)象
- 需要頻繁使用的對(duì)象献烦,可以通過(guò)緩存池復(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ù)加載圖片
- 字符串相加或者拼接通過(guò)StringBuilder替代舶替,較少創(chuàng)建String對(duì)象節(jié)省內(nèi)存
- 使用SpareArray、ArrayMap替代HashMap
內(nèi)存分析
LeakCannary
項(xiàng)目中依賴(lài)LeakCannary庫(kù)杠园,使用LeakCannary可以檢測(cè)內(nèi)存泄漏
Memory Profiler
使用Android Studio的內(nèi)存分析器可以對(duì)內(nèi)存分析顾瞪,根據(jù)分析結(jié)果進(jìn)行相應(yīng)的優(yōu)化
如需打開(kāi)內(nèi)存分析器,按以下步驟操作:
- 依次點(diǎn)擊View——》Tools windows——》Profiler(或者點(diǎn)擊工具欄中的Profiler圖標(biāo))
- 從Android Profiler工具欄中選擇要分析的設(shè)備和應(yīng)用進(jìn)程
- 點(diǎn)擊MEMORY時(shí)間軸上的任意位置打開(kāi)內(nèi)存性能分析器
打開(kā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)弥鹦、用戶(hù)輸入事件(如touch的down/press等)、屏幕旋轉(zhuǎn)事件等
5爷辙、內(nèi)存分類(lè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庫(kù)、字體等)
- others: 應(yīng)用使用了系統(tǒng)不確定如何分類(lèi)的內(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è)類(lèi)別分配的對(duì)象的內(nèi)存大小
捕獲堆轉(zhuǎn)儲(chǔ)
選擇內(nèi)存分析器中的Capture heap dump巍扛,點(diǎn)擊下方的Record按鈕领跛,就開(kāi)始捕獲堆轉(zhuǎn)儲(chǔ)了,可以點(diǎn)擊stop結(jié)束捕獲撤奸,結(jié)束捕獲之后會(huì)自動(dòng)加載捕獲到堆轉(zhuǎn)儲(chǔ)吠昭。下圖是捕獲到heap dump之后,打開(kāi)的界面
1胧瓜、過(guò)濾器
這部分主要用于對(duì)heap dump的數(shù)據(jù)進(jìn)行過(guò)濾矢棚,過(guò)濾我們關(guān)注、需要分享的部分府喳,包括
- 選擇需要檢查的堆類(lèi)型:
- 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ù)加載的類(lèi)
- view zygote heap: 檢查寫(xiě)時(shí)復(fù)制堆钝满,這部分是應(yīng)用通過(guò)zygote 創(chuàng)建啟動(dòng)進(jìn)程時(shí)的堆
我們應(yīng)用端一般主要分析view app heap進(jìn)行分析主堆兜粘,排在java層面的內(nèi)存問(wèn)題
- 選擇如何安排分派
- Arrang by class:默認(rèn),根據(jù)類(lèi)名稱(chēng)對(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過(guò)濾占用內(nèi)存占比比較高的類(lèi)進(jìn)行分析弯蚜,Arrang by package根據(jù)包名定位自己代碼孔轴、三方代碼的內(nèi)存問(wèn)題
- 選擇顯示那些類(lèi)型的數(shù)據(jù)
- Show all class: 默認(rèn),顯示所有的類(lèi)
- Show activity/fragment Leak: 顯示發(fā)生內(nèi)存泄漏的activity/fragment
- Show project class: 進(jìn)顯示項(xiàng)目相關(guān)的類(lèi)
- 輸入過(guò)濾:在輸入框中可以輸入類(lèi)名/包名來(lái)快速定位到具體類(lèi)/包名下類(lèi)的內(nèi)存分配情況
2碎捺、統(tǒng)計(jì)信息
- classes: 類(lèi)類(lèi)型總數(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ì)列舉過(guò)濾之后的所有類(lèi)名、分配的對(duì)象數(shù)及內(nèi)存使用情況晋柱,包括
- Class Name: 類(lèi)名
- Allocations: 此類(lèi)創(chuàng)建的實(shí)例對(duì)象數(shù)量
- Native Size: 此類(lèi)總共使用的原生內(nèi)存總量(只有android7.0+設(shè)備才能看到)(單位字節(jié))
- Shallow Size: 此類(lèi)使用的java內(nèi)存總量(單位字節(jié))
- Retained Size: 此類(lèi)實(shí)例對(duì)象仍存活而保留的內(nèi)存總大杏殴埂(單位字節(jié))
4、類(lèi)實(shí)例對(duì)象列表及其實(shí)例對(duì)象的詳細(xì)信息
在3中點(diǎn)擊某一個(gè)類(lèi)雁竞,會(huì)在下半部分顯示此類(lèi)的所有實(shí)例對(duì)象的信息钦椭,如點(diǎn)擊圖中的bitmap。
這部分左側(cè)顯示類(lèi)的實(shí)例對(duì)象列表:實(shí)例對(duì)象+地址浓领;點(diǎn)擊某個(gè)實(shí)例會(huì)在右側(cè)顯示此實(shí)例內(nèi)存分配的詳細(xì)信息玉凯,包括:
-
Fields
實(shí)例對(duì)象每個(gè)字段信息,包括如下信息:- Instance 此字段的名稱(chēng)及其類(lèi)型联贩,如果是基本數(shù)據(jù)類(lèi)型和String會(huì)同時(shí)顯示此字段的當(dāng)前值
- Depth: 此字段字段可達(dá)的最短跳數(shù)漫仆,表示的是任意一個(gè)GC Root到此字段的最短鏈路邊數(shù)
- Native Size: 原生內(nèi)存中此字段的內(nèi)存大小(只有Android7.0+上的設(shè)備才會(huì)看到此列)
- Shallow Size: Java 內(nèi)存中此字段的內(nèi)存大小
- Retained Size: 此字段目前還保留的內(nèi)存大小
-
References:
實(shí)例對(duì)象的引用鏈信息泪幌,References中包括如下信息:- Reference: 實(shí)例對(duì)象的引用鏈盲厌,可以依次點(diǎn)擊展開(kāi)顯示此實(shí)例被哪些實(shí)例對(duì)象所引用,通過(guò)引用鏈可以最終追蹤到GC Root
- Depth: 此實(shí)例對(duì)象可達(dá)的最短跳數(shù)祸泪,表示的是任意一個(gè)GC Root到此實(shí)例對(duì)象的最短鏈路邊數(shù)
- Native Size: 原生內(nèi)存中此實(shí)例對(duì)象的內(nèi)存大新鸷啤(只有Android7.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)存占比比較高的類(lèi)及其實(shí)例對(duì)象引用鏈情況阀湿,排查是否有內(nèi)存泄漏、是否有優(yōu)化空間
- 關(guān)注某些類(lèi)的實(shí)例對(duì)象數(shù)量比較大情況瑰妄,排查是存在大量創(chuàng)建短時(shí)間內(nèi)又銷(xiāo)毀引起內(nèi)存抖動(dòng)陷嘴,結(jié)合源碼分析是否有優(yōu)化空間,如使用緩存池等
- 關(guān)注activity间坐、context灾挨、view、Drawable等對(duì)象及其引用鏈情況竹宋,排查這些是否存在內(nèi)存泄漏
- 關(guān)注項(xiàng)目相關(guān)類(lèi)的內(nèi)存分配及其應(yīng)用鏈情況劳澄,排查是否存在內(nèi)存泄漏、使用不當(dāng)情況等