內(nèi)存泄漏
內(nèi)存管理???????????????????????????????????
內(nèi)存模型
??? Android原生開發(fā)以java為主。
在java中,Java內(nèi)存模型,往往是指Java程序在運(yùn)行時(shí)內(nèi)存的模型,而Java代碼是運(yùn)行在Java虛擬機(jī)之上的,所以Java內(nèi)存模型,也就是指Java虛擬機(jī)的運(yùn)行時(shí)內(nèi)存模型。
?java中內(nèi)存全權(quán)交給虛擬機(jī)去管理,那虛擬機(jī)的運(yùn)行時(shí)內(nèi)存是如何構(gòu)成的?
很多時(shí)候,我們提到內(nèi)存,會(huì)說到堆和棧,這是對(duì)內(nèi)存粗略的一種劃分,這種劃分的”堆”對(duì)應(yīng)內(nèi)存模型的Java堆,”?!笔侵柑摂M機(jī)棧,但是實(shí)際上Java內(nèi)存模型比這復(fù)雜多了。
?????? 在曾經(jīng)的日公司(sun 已被甲骨文2009年收購) 制定的java虛擬機(jī)規(guī)范中,運(yùn)行時(shí)內(nèi)存模型,分為線程私有和共享數(shù)據(jù)區(qū)兩大類,其中線程私有的數(shù)據(jù)區(qū)包含程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法區(qū),所有線程共享的數(shù)據(jù)區(qū)包含Java堆、方法區(qū),在方法區(qū)內(nèi)有一個(gè)常量池。


2.1 程序計(jì)數(shù)器PC
程序計(jì)數(shù)器PC是一塊較小的內(nèi)存空間,可以看作所執(zhí)行字節(jié)碼的行號(hào)指示器。字節(jié)碼解釋器就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,比如循環(huán)、跳轉(zhuǎn)、異常處理等等這些基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
在開發(fā)多線程應(yīng)用時(shí),由于Java中的多線程是搶占式的調(diào)用,也就是任何一個(gè)確定的時(shí)刻,cpu都只會(huì)執(zhí)行一條線程,執(zhí)行哪條線程也是不確定的。所以為了線程切換后能夠回到正確的執(zhí)行位置,每個(gè)線程都需要一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),所以這塊區(qū)域是”線程私有”的內(nèi)存。
當(dāng)線程正在執(zhí)行一個(gè)Java方法時(shí),PC計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼的地址;當(dāng)線程正在執(zhí)行的一個(gè)Native方法時(shí),PC計(jì)數(shù)器則為空(Undefined)。這一塊的內(nèi)存區(qū)域是唯一一個(gè)在java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError的區(qū)域。
2.2 虛擬機(jī)棧
和程序計(jì)數(shù)器一樣,虛擬機(jī)棧也是線程私有的,它的生命周期與線程相同。虛擬機(jī)棧描述的是java方法執(zhí)行的內(nèi)存模型。
每個(gè)方法(不包含native方法)執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀 用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。
?????? 我們平時(shí)所說的棧內(nèi)存就是指的這一塊區(qū)域。
Java虛擬機(jī)規(guī)范規(guī)定該區(qū)域有兩種異常:
StackOverFlowError:當(dāng)線程請求棧深度超出虛擬機(jī)棧所允許的深度時(shí)拋出 (遞歸函數(shù))
OutOfMemoryError:當(dāng)Java虛擬機(jī)動(dòng)態(tài)擴(kuò)展到無法申請足夠內(nèi)存時(shí)拋出(OOM)
2.3 本地方法棧
本地方法棧和虛擬機(jī)棧差不多,前者是為虛擬機(jī)使用到的Native方法提供內(nèi)存空間。有些虛擬機(jī)的實(shí)現(xiàn)直接把本地方法棧和虛擬機(jī)棧合二為一,比如主流的HotSpot虛擬機(jī)。
異常(Exception):Java虛擬機(jī)規(guī)范規(guī)定該區(qū)域可拋出StackOverFlowError和OutOfMemoryError。
2.4 Java堆
Java堆,是Java虛擬機(jī)管理的最大的一塊內(nèi)存,也是GC的主戰(zhàn)場,所以可以叫它gc堆(垃圾堆),里面存放的是幾乎所有的對(duì)象實(shí)例和數(shù)組數(shù)據(jù)。
異常(Exception):Java虛擬機(jī)規(guī)范規(guī)定該區(qū)域可拋出OutOfMemoryError。
2.5 方法區(qū)
方法區(qū)主要存放的是已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、編譯器編譯后的代碼等數(shù)據(jù)。Java虛擬機(jī)規(guī)范對(duì)這一塊區(qū)域的限制非常寬松,不同的虛擬機(jī)實(shí)現(xiàn)也不同,相對(duì)而言垃圾回收在這個(gè)區(qū)域比較少的出現(xiàn)。根據(jù)java虛擬機(jī)規(guī)范,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí),會(huì)拋出oom異常。
2.6 運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池是方法區(qū)的一部分,用于存放編譯器生成的各種字面量和符號(hào)引用。運(yùn)行時(shí)常量池除了編譯期產(chǎn)生的Class文件的常量池,還可以在運(yùn)行期間,將新的常量加入常量池,比較String類的intern()方法。
字面量:與Java語言層面的常量概念相近,包含文本字符串、聲明為final的常量值等。
符號(hào)引用:編譯語言層面的概念,包括以下3類:
類和接口的全限定名
字段的名稱和描述符
方法的名稱和描述符
屬于方法區(qū)一部分,所以和方法區(qū)一樣,會(huì)oom。
局部變量的基本數(shù)據(jù)類型和引用存儲(chǔ)于棧中,引用的對(duì)象實(shí)體存儲(chǔ)于堆中。
——因?yàn)樗鼈儗儆诜椒ㄖ械淖兞浚芷陔S方法而結(jié)束。
成員變量全部存儲(chǔ)與堆中(包括基本數(shù)據(jù)類型,引用和引用的對(duì)象實(shí)體)
——因?yàn)樗鼈儗儆陬?,類?duì)象終究是要被new出來使用的。
我們說的內(nèi)存泄露,是針對(duì),也只針對(duì)堆內(nèi)存,他們存放的就是引用指向的對(duì)象實(shí)體。
內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是由垃圾收集器(Garbage Collection,GC)完成的,java程序員不需要通過調(diào)用函數(shù)來釋放內(nèi)存,但gc只能回收無用并且不再被其它對(duì)象引用的那些對(duì)象所占用的空間。
堆中幾乎存放著Java世界中所有的對(duì)象實(shí)例,垃圾收集器在對(duì)堆回收之前,第一件事情就是要確定這些對(duì)象哪些還“存活”著,哪些對(duì)象已經(jīng)“死去”(即不可能再被任何途徑使用的對(duì)象)
確定對(duì)象是否活著的方法有:
1、引用計(jì)數(shù)算法
?????? 1.1算法分析
引用計(jì)數(shù)是垃圾收集器中的早期策略。在這種方法中,堆中每個(gè)對(duì)象實(shí)例都有一個(gè)引用計(jì)數(shù)。當(dāng)一個(gè)對(duì)象被創(chuàng)建時(shí),且將該對(duì)象實(shí)例分配給一個(gè)變量,該變量計(jì)數(shù)設(shè)置為1。當(dāng)任何其它變量被賦值為這個(gè)對(duì)象的引用時(shí),計(jì)數(shù)加1(a = b,則b引用的對(duì)象實(shí)例的計(jì)數(shù)器+1),當(dāng)一個(gè)對(duì)象實(shí)例的某個(gè)引用超過了生命周期或者被設(shè)置為一個(gè)新值時(shí),對(duì)象實(shí)例的引用計(jì)數(shù)器減1。任何引用計(jì)數(shù)器為0的對(duì)象實(shí)例可以被當(dāng)作垃圾收集。當(dāng)一個(gè)對(duì)象實(shí)例被垃圾收集時(shí),它引用的任何對(duì)象實(shí)例的引用計(jì)數(shù)器減1。
1.2優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
引用計(jì)數(shù)收集器可以很快的執(zhí)行,交織在程序運(yùn)行中。對(duì)程序需要不被長時(shí)間打斷的實(shí)時(shí)環(huán)境比較有利。
缺點(diǎn):
無法檢測出循環(huán)引用。如父對(duì)象有一個(gè)對(duì)子對(duì)象的引用,子對(duì)象反過來引用父對(duì)象。這樣,他們的引用計(jì)數(shù)永遠(yuǎn)不可能為0.
1引用計(jì)數(shù)算法無法解決循環(huán)引用問題,例如:
public class Main {
????????public static void main(String[] args) {
????????????????MyObject object1 = new MyObject();
????????????????MyObject object2 = new MyObject();
????????????????object1.object = object2;
????????????????object2.object = object1;
????????????????object1 = null;
? ? ? ? ? ? ? ? object2 = null;
????}
}
最后面兩句將object1和object2賦值為null,也就是說object1和object2指向的對(duì)象已經(jīng)不可能再被訪問,但是由于它們互相引用對(duì)方,導(dǎo)致它們的引用計(jì)數(shù)器都不為0,那么垃圾收集器就永遠(yuǎn)不會(huì)回收它們。
2、可達(dá)性分析算法(主流方法)

可達(dá)性分析算法中,通過一系列的gc root為起始點(diǎn),從一個(gè)GC ROOT開始,尋找對(duì)應(yīng)的引用節(jié)點(diǎn),找到這個(gè)節(jié)點(diǎn)以后,繼續(xù)尋找這個(gè)節(jié)點(diǎn)的引用節(jié)點(diǎn),當(dāng)所有的引用節(jié)點(diǎn)尋找完畢之后,剩余的節(jié)點(diǎn)則被認(rèn)為是沒有被引用到的節(jié)點(diǎn),即無用的節(jié)點(diǎn)。
java中可作為GC Root的對(duì)象有
1.虛擬機(jī)棧(本地變量表)中正在運(yùn)行使用的引用
2.方法區(qū)中靜態(tài)屬性引用的對(duì)象
3. 方法區(qū)中常量引用的對(duì)象
4.本地方法棧JNI中引用的對(duì)象(Native對(duì)象)
上圖中objD與objE到GC ROOT不可達(dá),所以可以被回收。而其他的對(duì)gc root可達(dá)。
在代碼看來,類似于:(GC/Main.java)

但是即使在可達(dá)性分析算法中不可達(dá)的對(duì)象,也并非一定要死。當(dāng)gc第一次掃過這些對(duì)象的時(shí)候,他們處于“死緩”的階段。要真正執(zhí)行死刑,至少需要經(jīng)過兩次標(biāo)記過程。如果對(duì)象經(jīng)過可達(dá)性分析之后發(fā)現(xiàn)沒有與GC Roots相關(guān)聯(lián)的引用鏈,那他會(huì)被第一次標(biāo)記,并經(jīng)歷一次篩選。這個(gè)對(duì)象的finalize方法會(huì)被執(zhí)行。如果對(duì)象沒有覆蓋finalize或者已經(jīng)被執(zhí)行過了。虛擬機(jī)也不會(huì)去執(zhí)行finalize方法。Finalize是對(duì)象逃獄的最后一次機(jī)會(huì)。
Reference項(xiàng)目的FinalizeEscapeGC


在例子中,對(duì)象第一次被執(zhí)行了finalize方法,但是把自己上交給國家逃了一死,但是在給國家執(zhí)行任務(wù)的時(shí)候,不幸犧牲了。所以沒辦法再自救了。
這個(gè)對(duì)象的finalize方法執(zhí)行了一次(自救而不是被救,this賦值,所以給的還是自己)
內(nèi)存泄漏
在說到內(nèi)存的問題,我們都會(huì)提到一個(gè)關(guān)鍵詞:引用。
??? 通俗的講,通過A能調(diào)用并訪問到B,那就說明A持有B的引用,或A就是B的引用。
?????? 比如 Person p1 = new
Person();通過P1能操作Person對(duì)象,因此P1是Person的引用;p1是類O中的一個(gè)成員變量,因此我們可以使用o.p1的方式來訪問Person類對(duì)象的成員,因此o持有一個(gè)Person對(duì)象的引用。
?????? GC過程與對(duì)象的引用類型是密切相關(guān)的,
Java對(duì)引用的分類Strong
reference(強(qiáng)), SoftReference(軟), WeakReference(弱), PhatomReference(虛)
強(qiáng)引用就是在程序代碼中普遍存在的,比如”O(jiān)bject obj = new Object()”這種引用,只要強(qiáng)引用還在,垃圾收集器就不會(huì)回收被引用的對(duì)象。
軟引用用來定義一些還有用但并非必須的對(duì)象。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要內(nèi)存溢出之前,會(huì)將這些對(duì)象列入回收范圍進(jìn)行第二次回收,如果回收后還是內(nèi)存不足,才會(huì)拋出內(nèi)存溢出。
弱引用也是用來描述非必須對(duì)象。但他的強(qiáng)度比軟引用更弱一些。被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器回收時(shí),無論內(nèi)存是否足夠,都會(huì)回收掉被弱引用關(guān)聯(lián)的對(duì)象。
虛引用也稱為幽靈引用或者幻影引用,是最弱的引用關(guān)系。一個(gè)對(duì)象的虛引用根本不影響其生存時(shí)間,也不能通過虛引用獲得一個(gè)對(duì)象實(shí)例。虛引用的唯一作用就是這個(gè)對(duì)象被GC時(shí)可以收到一條系統(tǒng)通知。

在Android應(yīng)用的開發(fā)中,為了防止內(nèi)存溢出,在處理一些占用內(nèi)存大而且生命周期較長的對(duì)象時(shí)候,可以盡量應(yīng)用軟引用和弱引用技術(shù)。
對(duì)于軟引用和弱引用的選擇,
如果只是想避免OutOfMemory異常的發(fā)生,則可以使用軟引用。如果對(duì)于應(yīng)用的性能更在意,想盡快回收一些占用內(nèi)存比較大的對(duì)象,則可以使用弱引用。另外可以根據(jù)對(duì)象是否經(jīng)常使用來判斷選擇軟引用還是弱引用。如果該對(duì)象可能會(huì)經(jīng)常使用的,就盡量用軟引用。如果該對(duì)象不被使用的可能性更大些,就可以用弱引用。
內(nèi)存泄漏就是
堆內(nèi)存中的長生命周期的對(duì)象持有短生命周期對(duì)象的引用,盡管短生命周期對(duì)象已經(jīng)不再需要,但是因為長生命周期對(duì)象持有它的引用而導(dǎo)致不能被回收,這就是Java中內(nèi)存泄露的根本原因。
?總結(jié)一句話就是不需要了該回收因為引用問題導(dǎo)致不能回收。
內(nèi)存泄漏會(huì)導(dǎo)致可用內(nèi)存慢慢變少,讓程序慢慢變卡。最終還會(huì)導(dǎo)致臭名昭著的oom 內(nèi)存溢出。
我們的android程序應(yīng)該怎么排查內(nèi)存泄漏問題呢?
在android中我們執(zhí)行一段代碼,比如進(jìn)入了一個(gè)新的頁面(Activity),這時(shí)候我們的內(nèi)存使用肯定比在前一個(gè)頁面大,而在界面finish返回后,如果內(nèi)存沒有回落,那么很有可能就是出現(xiàn)了內(nèi)存泄漏。
從內(nèi)存監(jiān)控工具中觀察內(nèi)存曲線,是否存在不斷上升的趨勢且不會(huì)在程序返回時(shí)明顯回落。這種方式可以發(fā)現(xiàn)最基本,也是最明顯的內(nèi)存泄露問題,對(duì)用戶價(jià)值最大,操作難度小,性價(jià)比極高。
因?yàn)樗馨l(fā)現(xiàn)很明顯很嚴(yán)重的內(nèi)存泄漏問題
我們可以通過AS的Memory Profile或者DDMS中的heap觀察內(nèi)存使用情況。
Android Profile
?? https://developer.android.com/studio/preview/features/android-profiler.html
?
????? The newAndroid
Profilerwindow in Android Studio 3.0 replaces theAndroid Monitortools.
?
?????? 在Android Studio我們可以點(diǎn)擊

運(yùn)行app。
然后我們能看到

我們點(diǎn)擊memory后


① 強(qiáng)制執(zhí)行垃圾收集事件的按鈕。
② 捕獲堆轉(zhuǎn)儲(chǔ)的按鈕。
③ 記錄內(nèi)存分配的按鈕。
④ 放大時(shí)間線的按鈕。
⑤ 跳轉(zhuǎn)到實(shí)時(shí)內(nèi)存數(shù)據(jù)的按鈕。
⑥ 事件時(shí)間線顯示活動(dòng)狀態(tài)、用戶輸入事件和屏幕旋轉(zhuǎn)事件。
⑦ 內(nèi)存使用時(shí)間表,其中包括以下內(nèi)容:
每個(gè)內(nèi)存類別使用多少內(nèi)存的堆棧圖,如左邊的y軸和頂部的顏色鍵所示。
虛線表示已分配對(duì)象的數(shù)量,如右側(cè)y軸所示。
每個(gè)垃圾收集事件的圖標(biāo)。
與之前的Android監(jiān)控工具相比,新的內(nèi)存分析器記錄了更多內(nèi)存使用情況,所以看起來你的內(nèi)存使用量會(huì)更高。內(nèi)存分析器監(jiān)視一些額外的類別,這些類別增加了總數(shù)。
在比較茫然的情況下,不知道哪兒出現(xiàn)了內(nèi)存泄漏,我們可以進(jìn)行一刀切,來個(gè)大致的排查。
我們進(jìn)入了一大堆頁面并最終返回到主頁。
然后gc ,再dump下內(nèi)存查看。AS可以點(diǎn)擊

然后等待一段時(shí)間會(huì)出現(xiàn):

但是說實(shí)話這個(gè)頁面想要分析出什么很難。一般不會(huì)使用這個(gè)頁面來進(jìn)行分析,最多打開看一眼剛剛我們進(jìn)入的Activity是否因?yàn)槲覀兺顺龆厥铡?/p>
先按照包名來分組,

Alloc Cout : 對(duì)象數(shù)
Shallow Size : 對(duì)象占用內(nèi)存大小
Retained Set : 對(duì)象引用組占用內(nèi)存大小(包含了這個(gè)對(duì)象引用的其他對(duì)象)
當(dāng)然一次dump可能并不能發(fā)現(xiàn)內(nèi)存泄漏,可能每次我們dump的結(jié)果都不同,那么就需要多試幾次,然后結(jié)合代碼來排查。
這里還不能確定發(fā)生了內(nèi)存泄漏。
我們這時(shí)候可以借助一些更專業(yè)的工具來進(jìn)行內(nèi)存的分析。
我們先把

這個(gè)內(nèi)存快照保存為hprof文件。
AS自動(dòng)分析
其實(shí)現(xiàn)在的AS,可以說是非常強(qiáng)大了。我們把剛剛保存的hprof文件拖入到AS中。


這個(gè)自動(dòng)分析任務(wù)包含了兩個(gè)內(nèi)容,一個(gè)是檢測Activity的泄漏,一個(gè)是檢測重復(fù)字符串。
點(diǎn)擊運(yùn)行分析:

這里出現(xiàn)了MainActivity的泄漏。并且觀察到這個(gè)MainActivity可能不止一個(gè)對(duì)象存在,可能是我們上次退出程序的時(shí)候發(fā)生了泄漏,導(dǎo)致它不能回收。而在此打開app,系統(tǒng)會(huì)創(chuàng)建新的MainActivity。
但是在AS上發(fā)現(xiàn)為何沒被回收需要運(yùn)氣,更方便的工具是Mat。
Memory Analyzer Tool基于eclipse
可以直接下載:
http://www.eclipse.org/mat/downloads.php
也可以在eclispe上安裝mat插件:

點(diǎn)擊eclipse? ? marketplace...搜索memory。

在使用mat之前我們需要把快照文件轉(zhuǎn)換一下,
轉(zhuǎn)換工具在sdk/platform-tools/hprof-conv
-z:排除不是app的內(nèi)存,比如Zygote
hprof-conv -z src dst
然后在Mat中打開它:

打開我們的快照文件,
之后我們能看到

我們點(diǎn)擊

以直方圖的方式來顯示當(dāng)前內(nèi)存使用情況可能更加適合較為復(fù)雜的內(nèi)存泄漏分析,它默認(rèn)直接顯示當(dāng)前內(nèi)存中各種類型對(duì)象的數(shù)量及這些對(duì)象的shallow heap和retained heap。結(jié)合MAT提供的不同顯示方式,往往能夠直接定位問題
shallow heap:指的是某一個(gè)對(duì)象所占內(nèi)存大小。
retained heap:指的是一個(gè)對(duì)象與所包含對(duì)象所占內(nèi)存的總大小。
out查看這個(gè)對(duì)象持有的外部對(duì)象引用。
incoming查看這個(gè)對(duì)象被哪些外部對(duì)象引用。
我們現(xiàn)在希望查看為什么這個(gè)對(duì)象還存在,那么

排除軟弱虛引用。

關(guān)于這個(gè)問題是android系統(tǒng)的一個(gè)bug。
原因是Activity的DecorView請求了InputMethodManager,而InputMethodManager一直持有DecorView的引用,導(dǎo)致無法釋放Activity。
解決辦法是:


這個(gè)問題是一個(gè)第三方庫中持有引用,導(dǎo)致的無法釋放。
這個(gè)問題可能是這個(gè)第三方庫本身的問題,也可能是我們使用不當(dāng)引起的。為了確定這個(gè)問題。我們進(jìn)入被混淆了的g.e這個(gè)類,
注意這里是g.e中的a成員是一個(gè)HashMap,

結(jié)合代碼

這里面保存的都是一些Observer對(duì)象。這里就需要結(jié)合對(duì)代碼的熟悉度,加上一些猜測來尋找問題。
在Activity中搜索Observer,可以找到很多個(gè)new Observer。
但是我們注意,調(diào)用的地方第二個(gè)參數(shù)是true,表示注冊;如果傳false表示注銷

而這里只有注冊,沒有注銷。
我們在onDestory中加入注銷。
修改完成再次運(yùn)行,然后退出app一次再打開:
結(jié)果:

還有兩個(gè)Activity。
第一個(gè)問題仍然是observer,但是是在MessageFragment。
第二個(gè)問題也還是InputMethodmanager,但是泄漏點(diǎn)變了。
修改:

內(nèi)存泄漏解決完成!
??? 這個(gè)app還有很多內(nèi)存泄漏的地方。大家可以自己嘗試去尋找并解決。比如MyInfoActivity。。。。大家自己去解決啊,有什么不懂得再問我。如果問的人多,下節(jié)課先解決掉這個(gè)泄漏。
除了檢查單個(gè)hprof文件之外,還能夠使用多個(gè)hprof進(jìn)行對(duì)比。
比如我們在進(jìn)入一個(gè)新頁面之前先dump下來內(nèi)存。然后再進(jìn)入這個(gè)頁面之后退出,再dump一份內(nèi)存。通過對(duì)比就能夠知道,進(jìn)入這個(gè)頁面之后增加了多少內(nèi)存/對(duì)象等信息:
之后在mat中打開這個(gè)兩個(gè)文件并都切換到直方圖查看。
再然后把兩個(gè)直方均加入對(duì)比,點(diǎn)擊

或者

執(zhí)行對(duì)比:

再把視圖切換到difference from base table(與第一個(gè)的不同)

然后能看到

第二行就是指第二個(gè)文件相比第一個(gè)文件多出來了幾個(gè)對(duì)象。
如果存在增加了不合理的對(duì)象,同樣可以查看其GC root。???
=====================================================================
對(duì)Android內(nèi)存泄露 我們還可以使用著名的LeakCanary
(Square出品,Square可謂Android開源界中的業(yè)界良心,開源的項(xiàng)目包括okhttp, retrofit,otto, picasso, Android開發(fā)大神Jake Wharton曾今就是Square)來進(jìn)行檢測
https://github.com/square/leakcanary
這個(gè)庫也有一些bug,但總體來說還是能起到一定的輔助作用。
總結(jié):
內(nèi)存泄漏常見原因:
1.集合類
集合類如果僅僅有添加元素的方法,而沒有相應(yīng)的刪除機(jī)制,導(dǎo)致內(nèi)存被占用。如果這個(gè)集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它),那么沒有相應(yīng)的刪除機(jī)制,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。
?????? 一開始的微信工程中使用的ButterKnife中的linkeahashmap就存在這個(gè)問題。
2、靜態(tài)成員
?????? Static成員作為gc root,如果一個(gè)對(duì)象被static聲明,這個(gè)對(duì)象會(huì)一直存活直到程序進(jìn)程停止。
2.單例模式
不正確使用單例模式是引起內(nèi)存泄露的一個(gè)常見問題,單例對(duì)象在被初始化后將在 JVM 的整個(gè)生命周期中存在(以靜態(tài)變量的方式),如果單例對(duì)象持有外部對(duì)象的引用,那么這個(gè)外部對(duì)象將不能被 JVM 正常回收,導(dǎo)致內(nèi)存泄露。

這里如果傳遞Activity作為Context來獲得單例對(duì)象,那么單例持有Activity的引用,導(dǎo)致Activity不能被釋放。
不要直接對(duì) Activity 進(jìn)行直接引用作為成員變量,如果允許可以使用Application。如果不得不需要Activity作為Context,可以使用弱引用WeakReference,相同的,對(duì)于Service 等其他有自己聲明周期的對(duì)象來說,直接引用都需要謹(jǐn)慎考慮是否會(huì)存在內(nèi)存泄露的可能。
3.未關(guān)閉/釋放資源
BraodcastReceiver,ContentObserver,F(xiàn)ileObserver,Cursor,Callback等在 Activity
onDestroy 或者某類生命周期結(jié)束之后一定要 unregister 或者 close 掉,否則這個(gè) Activity 類會(huì)被 system 強(qiáng)引用,不會(huì)被內(nèi)存回收。
?????? 我們經(jīng)常會(huì)寫出下面的代碼

當(dāng)然這樣寫代碼沒問題,但是如果我們在close之前還有一些可能拋出異常的代碼

那么現(xiàn)在這段代碼存在隱患的。因?yàn)槿绻\(yùn)行到fos2時(shí)候拋出了異常,那么fos也沒辦法close。
所以正確的方式應(yīng)該是

因?yàn)槿绻鹷rite發(fā)生異常那么這個(gè)fos會(huì)因?yàn)闆]有close造成內(nèi)存泄漏。
4.
Handler
只要 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對(duì)象將被線程 MessageQueue 一直持有。特別是handler執(zhí)行延遲任務(wù)。所以,Handler 的使用要尤為小心,否則將很容易導(dǎo)致內(nèi)存泄露的發(fā)生。

這種創(chuàng)建Handler的方式會(huì)造成內(nèi)存泄漏,由于mHandler是Handler的非靜態(tài)匿名內(nèi)部類的實(shí)例,所以它持有外部類Activity的引用,我們知道消息隊(duì)列是在一個(gè)Looper線程中不斷輪詢處理消息,那么當(dāng)這個(gè)Activity退出時(shí)消息隊(duì)列中還有未處理的消息或者正在處理消息,而消息隊(duì)列中的Message持有mHandler實(shí)例的引用,mHandler又持有Activity的引用,所以導(dǎo)致該Activity的內(nèi)存資源無法及時(shí)回收,引發(fā)內(nèi)存泄漏,所以另外一種做法為:

創(chuàng)建一個(gè)靜態(tài)Handler內(nèi)部類,然后對(duì)Handler持有的對(duì)象使用弱引用,這樣在回收時(shí)也可以回收Handler持有的對(duì)象,這樣雖然避免了Activity泄漏,不過Looper線程的消息隊(duì)列中還是可能會(huì)有待處理的消息,所以我們在Activity的Destroy時(shí)或者Stop時(shí)應(yīng)該移除消息隊(duì)列中的消息,

使用mHandler.removeCallbacksAndMessages(null);是移除消息隊(duì)列中所有消息和所有的Runnable。當(dāng)然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();來移除指定的Runnable和Message。
5.
Thread 內(nèi)存泄露
和handler一樣,線程也是造成內(nèi)存泄露的一個(gè)重要的源頭。線程產(chǎn)生內(nèi)存泄露的主要原因在于線程生命周期的不可控。比如線程是 Activity 的內(nèi)部類,則線程對(duì)象中保存了 Activity 的一個(gè)引用,當(dāng)線程的 run 函數(shù)耗時(shí)較長沒有結(jié)束時(shí),線程對(duì)象是不會(huì)被銷毀的,因此它所引用的老的 Activity 也不會(huì)被銷毀,因此就出現(xiàn)了內(nèi)存泄露的問題。
Thread和Handler都可以劃分到為非靜態(tài)包括匿名內(nèi)部類的內(nèi)存泄漏。
6、系統(tǒng)bug,比如InputMethodManager