java的內(nèi)存分配
靜態(tài)存儲區(qū):編譯時(shí)就分配好蟆融,在程序整個(gè)運(yùn)行期間都存在瘦真。它主要存放靜態(tài)數(shù)據(jù)和常量花枫;
棧區(qū):當(dāng)方法執(zhí)行時(shí),會在棧區(qū)內(nèi)存中創(chuàng)建方法體內(nèi)部的局部變量尚困,方法結(jié)束后自動釋放內(nèi)存蠢箩;
堆區(qū):通常用來存放new出來的對象。由java垃圾回收期回收事甜。
四種引用類型的介紹
強(qiáng)引用(StrongReference):JVM 寧可拋出 OOM 谬泌,也不會讓 GC 回收具有強(qiáng)引用的對象;
軟引用(SoftReference):只有在內(nèi)存空間不足時(shí)逻谦,才會被回的對象掌实;
弱引用(WeakReference):在 GC 時(shí),一旦發(fā)現(xiàn)了只具有弱引用的對象邦马,不管當(dāng)前內(nèi)存空間足夠與否贱鼻,都會回收它的內(nèi)存宴卖;
虛引用(PhantomReference):任何時(shí)候都可以被GC回收,當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對象時(shí)邻悬,如果發(fā)現(xiàn)它還有虛引用症昏,就會在回收對象的內(nèi)存之前,把這個(gè)虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中父丰。程序可以通過判斷引用隊(duì)列中是否存在該對象的虛引用肝谭,來了解這個(gè)對象是否將要被回收《晟龋可以用來作為GC回收Object的標(biāo)志攘烛。
內(nèi)存抖動
這樣的圖很熟悉有木有?當(dāng)這樣的時(shí)候镀首,說明你的內(nèi)存真的在變少了坟漱。
內(nèi)存泄漏檢測工具
1.MAT(Memory Analyzer Tool),下載地址:
http://www.eclipse.org/mat/downloads.php
具體使用這個(gè)網(wǎng)上一大篇蘑斧,樓主一有時(shí)間也會為大家一步步奉上靖秩。
2.強(qiáng)大的開源內(nèi)存檢測工具 LeakCanary。
leakcanary 是一個(gè)開源項(xiàng)目竖瘾,一個(gè)內(nèi)存泄露自動檢測工具沟突,是著名的 GitHub 開源組織 Square 貢獻(xiàn)的,它的主要優(yōu)勢就在于自動化過早的發(fā)覺內(nèi)存泄露捕传、配置簡單惠拭、抓取貼心,缺點(diǎn)在于還存在一些bug庸论,不過正常使用百分之九十情況是OK的职辅,其核心原理與MAT工具類似。
因?yàn)榕渲檬趾唵文羰荆恍枰獛拙湓捑秃茫域携。∮愫恚∵@里就不多說了秀鞭,大家可以看官方文檔:
https://github.com/square/leakcanary
內(nèi)存泄漏解析
永遠(yuǎn)的Singleton
單例的使用在我們的程序中隨處可見,因?yàn)槭褂盟梢酝昝赖慕鉀Q我們在程序中重復(fù)創(chuàng)建對象的問題扛禽,不過可別小瞧它锋边。由于單例的靜態(tài)特性,使得它的生命周期和應(yīng)用的生命周期會一樣長编曼,所以一旦使用有誤豆巨,小心無限制的持有Activity的引用而導(dǎo)致內(nèi)存泄漏。比如掐场,下面的例子往扔。
這個(gè)錯誤在生活中再普遍不過瓤球,很正常的一個(gè)單例模式敏弃,可就由于傳入的是一個(gè) Context卦羡,而這個(gè) Context 的生命周期的長短就尤為重要了。如果我們傳入的是 Activity 的 Context麦到,當(dāng)這個(gè) Context 所對應(yīng)的 Activity 退出的時(shí)候,由于該 Context 的引用被單例對象所持有拟赊,其生命周期等于整個(gè)應(yīng)用程序的生命周期,所以當(dāng)前 Activity 退出時(shí)它的內(nèi)存并不會回收粹淋,這造成的內(nèi)存泄漏就可想而知了吸祟。
正確的方式應(yīng)該是把傳入的 Context 換為和應(yīng)用的生命周期一樣長的 Application 的 Context:
當(dāng)然桃移,你也可以直接連 Context 都不用傳入了。重寫 application借杰,提供靜態(tài)的 getContext 方法:
自然就可以直接不用傳入Context:
令人心塞的Handler
這個(gè)東西在我最近遇到的最多了蔗衡,而它也是我們在內(nèi)存泄漏中最為常見的,也許你的一個(gè)小忽略就會導(dǎo)致內(nèi)存泄漏绞惦。在Android的新版本中,我們被要求必須把網(wǎng)絡(luò)任務(wù)等耗時(shí)操作置于新線程來處理杰刽,我們通常會采用 Handler堆生。
但 Handler 不是萬能的,若是我們的編寫不規(guī)范就有可能會造成內(nèi)存泄漏涝婉。另外蔗怠,我們知道吩跋,Handler渔工、Message 和 MessageQueue 都是相互關(guān)聯(lián)在一起的,萬一 Handler 發(fā)送的 Message 尚未被處理梁丘,則該 Message 及發(fā)送它的 Handler 對象將會被線程 MessageQueue 一直持有旺韭。
由于 Handler 屬于 TLS(Thread Local Storage)變量,生命周期和 Activity 是不一致的值漫。因此這種實(shí)現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期一致织盼,故很容易導(dǎo)致無法正確釋放。比如:
在例子中危虱,我們申明了一個(gè)延遲5分鐘執(zhí)行的消息 Message谋国。當(dāng)該 Activity 被 finish 的時(shí)候,延遲任務(wù)的 Message 還存在于主線程中捌蚊,它持有該 Activity 的 Handler 引用近弟,所以此時(shí) finish 掉的 Activity 就不會回收了缅糟,所以造成了內(nèi)存泄漏(因 handler 為非靜態(tài)內(nèi)部類祷愉,它會持有外部類的引用,在這里就是當(dāng)前的 Activity)赴涵。
修復(fù):這個(gè)解決也是可以通過把其聲明為 static 的订讼,則其存活期就跟 activity 的生命周期無關(guān)了。不過倘若用到 Context 等外部類的 非static 對象,還是應(yīng)該通過弱引用傳入鳖敷。比如:
綜述:推薦使用靜態(tài)內(nèi)部類+弱引用 WeakReference 這種方式程拭,但要注意每次使用前判空。
說到若引用崖媚,這里再提下java的幾種引用類型:Strong, reference,SoftReference,WeakReference 和 PhatomReference:
在 Android 開發(fā)中山宾,為了防止內(nèi)存溢出鳍徽,在處理一些占用內(nèi)存大并且生命周期較長的對象的時(shí)候,可以盡量地使用 軟引用 和 弱引用 技術(shù)绷杜。
比如濒募,保存 Bitmap 的軟引用到 HashMap:
使用軟引用以后瑰剃,在 OutOfMemory 異常發(fā)生之前,這些緩存的圖片資源的內(nèi)存空間可以被釋放掉的晌姚,從而避免內(nèi)存達(dá)到上限挥唠,避免Crash發(fā)生。
如果只是想避免 OutOfMemory 異常的發(fā)生宝磨,則可以使用 軟引用唤锉。如果對于應(yīng)用的性能更在意,想盡快回收一些占用內(nèi)存比較大的對象窿祥,則可以使用 弱引用壁肋。
另外可以根據(jù)對象是否經(jīng)常使用來判斷選擇 軟引用 還是 弱引用籽慢。如果該對象可能會經(jīng)常使用的猫胁,就盡量用 軟引用。如果該對象不被使用的可能性更大些届惋,就可以用 弱引用菠赚。
ok,繼續(xù)回到主題瘩欺。前面所說的拌牲,創(chuàng)建一個(gè) 靜態(tài)Handler內(nèi)部類,然后對 Handler 持有的對象使用弱引用拍埠,這樣在回收時(shí)也可以回收 Handler 持有的對象土居,但是這樣做雖然避免了 Activity 泄漏,不過 Looper 線程的消息隊(duì)列中還是可能會有待處理的消息棉圈,所以我們在 Activity 的 Destroy 時(shí)或者 Stop 時(shí)應(yīng)該移除消息隊(duì)列 MessageQueue 中的消息埂奈。
下面幾個(gè)方法都可以移除 Message:
匿名內(nèi)部類/非靜態(tài)內(nèi)部類
它們方便卻暗藏殺機(jī)账磺。Android開發(fā)經(jīng)常會繼承實(shí)現(xiàn) Activity 或者 Fragment 或者 View。如果你使用了匿名類氏捞,而又被異步線程所引用冒版,那得小心,如果沒有任何措施同樣會導(dǎo)致內(nèi)存泄漏的:
runnable1和runnable2的區(qū)別就是捆等,runnable2 使用了匿名內(nèi)部類,我們看看引用時(shí)的引用內(nèi)存:
可以看到谒养,runnable1 是沒有什么特別的明郭。但 runnable2 多出了一個(gè) MainActivity 的引用,若是這個(gè)引用再傳入到一個(gè)異步線程始绍,此線程在和Activity生命周期不一致的時(shí)候话侄,也就造成了Activity的泄露。
前輩箴言——善用static成員變量
前面就很明顯径簿,當(dāng)我們的成員變量是static的時(shí)候嘀韧,那么它的生命周期將和整個(gè)app的生命周期一致缠捌。
這必然會導(dǎo)致一系列問題曼月,如果你的app進(jìn)程設(shè)計(jì)上是長駐內(nèi)存的,那即使app切到后臺炎辨,這部分內(nèi)存也不會被釋放聪姿。按照現(xiàn)在手機(jī)app內(nèi)存管理機(jī)制,占內(nèi)存較大的后臺進(jìn)程將優(yōu)先回收末购,因?yàn)槿绻薬pp做過進(jìn)程互保泵肆瘢活,那會造成app在后臺頻繁重啟羽德。當(dāng)手機(jī)安裝了你參與開發(fā)的app以后一夜時(shí)間手機(jī)被消耗空了電量、流量宅静,你的app不得不被用戶卸載或者靜默坏为。
這里修復(fù)的方法是:
不要在類初始時(shí)初始化靜態(tài)成員≡确可以考慮 lazy初始化(延遲加載)够颠。架構(gòu)設(shè)計(jì)上要思考是否真的有必要這樣做,盡量避免履磨。如果架構(gòu)需要這么設(shè)計(jì)剃诅,那么此對象的生命周期你有責(zé)任管理起來。
遠(yuǎn)離非靜態(tài)內(nèi)部類和匿名類笑跛,多用private static class
在我們的日常代碼中聊品,這樣的情況似乎很常見,及直接寫一個(gè)class就這么光禿禿的情況陈哑。
這樣就在 Activity 內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例伸眶,每次啟動 Activity 時(shí)都會使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復(fù)創(chuàng)建爬坑,不過這種寫法卻會造成內(nèi)存泄漏涂臣,因?yàn)榉庆o態(tài)內(nèi)部類默認(rèn)會持有外部類的引用,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個(gè)靜態(tài)的實(shí)例署辉,該實(shí)例的生命周期和應(yīng)用的一樣長哭尝,這就導(dǎo)致了該靜態(tài)實(shí)例一直會持有該 Activity 的引用,導(dǎo)致 Activity 的內(nèi)存資源不能正巢酿校回收桶唐。正確的做法為:
將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個(gè)單例,如果需要使用 Context尤泽,請按照上面推薦的使用 Application 的 Context坯约。當(dāng)然,Application 的 context ?不是萬能的横殴,所以也不能隨便亂用妇智,對于有些地方則必須使用 Activity 的 Context,對于Application,Service蛋欣,Activity三者的 Context 的應(yīng)用場景如下:
其中: NO1 表示 Application 和 Service 可以啟動一個(gè) Activity陷虎,不過需要創(chuàng)建一個(gè)新的 task 任務(wù)隊(duì)列。而對于 Dialog 而言窝稿,只有在 Activity 中才能創(chuàng)建凿掂。
集合對象善清除纹蝴,以免內(nèi)存泄漏觸不及防
我們通常會把一些對象的引用加入到集合容器(比如 ArrayList)中塘安,當(dāng)我們不再需要該對象時(shí)援奢,并沒有把它的引用從集合中清理掉,這樣這個(gè)集合就會越來越大切黔。如果這個(gè)集合是 static 的話具篇,那情況就更嚴(yán)重了。
所以在退出程序之前险领,將集合里面的東西 clear秒紧,然后置為 null,再退出程序脐湾,如下:
webView雖火叙淌,內(nèi)存泄漏卻也火的其所
當(dāng)我們不再需要使用 webView 的時(shí)候鹰霍,應(yīng)該調(diào)用它的 destory() 方法來銷毀它,并釋放其占用的內(nèi)存孟岛,否則其占用的內(nèi)存長期也不能回收督勺,從而造成內(nèi)存泄漏。
解決方案:為 webView 開啟另外一個(gè)進(jìn)程次询,通過AIDL與主線程進(jìn)行通信瓷叫,webView 所在的進(jìn)程可以根據(jù)業(yè)務(wù)的需要選擇合適的時(shí)機(jī)進(jìn)行銷毀送巡,從而達(dá)到內(nèi)存的完整釋放雌芽。
而另外一些諸如 listView 的 Adapter 沒有緩存之類的這里就不再多提了世落。
做一個(gè)小的總結(jié)
構(gòu)造 Adapter 時(shí),沒有使用緩存的 convertView
Bitmap 對象不在使用時(shí)調(diào)用 recycle() 釋放內(nèi)存
Context 使用不當(dāng)造成內(nèi)存泄露:不要對一個(gè) Activity Context 保持長生命周期的引用谷朝。盡量在一切可以使用應(yīng)用 ApplicationContext 代替 Context 的地方進(jìn)行替換武花。
非靜態(tài)內(nèi)部類的靜態(tài)實(shí)例容易造成內(nèi)存泄漏:即一個(gè)類中如果你不能夠控制它其中內(nèi)部類的生命周期(譬如Activity中的一些特殊Handler等),則盡量使用靜態(tài)類和弱引用來處理(譬如ViewRoot的實(shí)現(xiàn))专钉。
警惕線程未終止造成的內(nèi)存泄露累铅;譬如在 Activity 中關(guān)聯(lián)了一個(gè)生命周期超過 Activity 的 Thread娃兽,在退出 Activity 時(shí)切記結(jié)束線程。一個(gè)典型的例子就是 HandlerThread 的 run 方法是一個(gè)死循環(huán)投储,它不會自己結(jié)束玛荞,線程的生命周期超過了 Activity 生命周期,我們必須手動在Activity的銷毀方法中中調(diào)運(yùn) thread.getLooper().quit(); 才不會泄露驹碍。
對象的注冊與反注冊沒有成對出現(xiàn)造成的內(nèi)存泄露凡恍;譬如注冊廣播接收器怔球、注冊觀察者(典型的譬如數(shù)據(jù)庫的監(jiān)聽)等。
創(chuàng)建與關(guān)閉沒有成對出現(xiàn)造成的泄露闽巩;譬如 Cursor 資源必須手動關(guān)閉,WebView 必須手動銷毀洼冻,流等對象必須手動關(guān)閉等隅很。
不要在執(zhí)行頻率很高的方法或者循環(huán)中創(chuàng)建對象(比如onmeasure)叔营,可以使用 HashTable 等創(chuàng)建一組對象容器從容器中取那些對象,而不用每次new與釋放畜挥。
避免代碼設(shè)計(jì)模式的錯誤造成內(nèi)存泄露婴谱;譬如循環(huán)引用,A持有B华糖,B持有C口糕,C持有A,這樣的設(shè)計(jì)誰都得不到釋放十办。