內(nèi)存的分配策略概述
程序運(yùn)行時(shí)的內(nèi)存分配有三種策略,分別是靜態(tài)的,棧式的,和堆式的牲尺,對(duì)應(yīng)的,三種存儲(chǔ)策略使用的內(nèi)存空間主要分別是靜態(tài)存儲(chǔ)區(qū)(也稱方法區(qū))幌蚊、堆區(qū)和棧區(qū)谤碳。
靜態(tài)存儲(chǔ)區(qū)(方法區(qū)):內(nèi)存在程序編譯的時(shí)候就已經(jīng)分配好,這塊內(nèi)存在程序整個(gè)運(yùn)行期間都存在溢豆。它主要存放靜態(tài)數(shù)據(jù)估蹄、全局static數(shù)據(jù)和常量。
棧區(qū):在執(zhí)行函數(shù)時(shí)沫换,函數(shù)內(nèi)局部變量的存儲(chǔ)單元都可以在棧上創(chuàng)建臭蚁,函數(shù)執(zhí)行結(jié)束時(shí)這些存儲(chǔ)單元自動(dòng)被釋放。棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中讯赏,效率很高垮兑,但是分配的內(nèi)存容量有限。
堆區(qū):亦稱動(dòng)態(tài)內(nèi)存分配漱挎。程序在運(yùn)行的時(shí)候用malloc或new申請(qǐng)任意大小的內(nèi)存系枪,程序員自己負(fù)責(zé)在適當(dāng)?shù)臅r(shí)候用free或delete釋放內(nèi)存(Java則依賴?yán)厥掌鳎?dòng)態(tài)內(nèi)存的生存期可以由我們決定磕谅,如果我們不釋放內(nèi)存私爷,程序?qū)⒃谧詈蟛裴尫诺魟?dòng)態(tài)內(nèi)存。 但是膊夹,良好的編程習(xí)慣是:如果某動(dòng)態(tài)內(nèi)存不再使用衬浑,需要將其釋放掉。
堆和棧的區(qū)別:
在函數(shù)中(說明是局部變量)定義的一些基本類型的變量和對(duì)象的引用變量都是在函數(shù)的棧內(nèi)存中分配放刨。當(dāng)在一段代碼塊中定義一個(gè)變量時(shí)工秩,java就在棧中為這個(gè)變量分配內(nèi)存空間,當(dāng)超過變量的作用域后,java會(huì)自動(dòng)釋放掉為該變量分配的內(nèi)存空間助币,該內(nèi)存空間可以立刻被另作他用浪听。
堆內(nèi)存用于存放所有由new創(chuàng)建的對(duì)象(內(nèi)容包括該對(duì)象其中的所有成員變量)和數(shù)組。在堆中分配的內(nèi)存眉菱,由java虛擬機(jī)自動(dòng)垃圾回收器來管理迹栓。在堆中產(chǎn)生了一個(gè)數(shù)組或者對(duì)象后,還可以在棧中定義一個(gè)特殊的變量俭缓,這個(gè)變量的取值等于數(shù)組或者對(duì)象在堆內(nèi)存中的首地址迈螟,在棧中的這個(gè)特殊的變量就變成了數(shù)組或者對(duì)象的引用變量,以后就可以在程序中使用棧內(nèi)存中的引用變量來訪問堆中的數(shù)組或者對(duì)象尔崔,引用變量相當(dāng)于為數(shù)組或者對(duì)象起的一個(gè)別名答毫,或者代號(hào)
堆是不連續(xù)的內(nèi)存區(qū)域(因?yàn)橄到y(tǒng)是用鏈表來存儲(chǔ)空閑內(nèi)存地址,自然不是連續(xù)的)季春,堆大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存(32bit系統(tǒng)理論上是4G)洗搂,所以堆的空間比較靈活,比較大载弄。棧是一塊連續(xù)的內(nèi)存區(qū)域耘拇,大小是操作系統(tǒng)預(yù)定好的,windows下棧大小是2M(也有是1M宇攻,在編譯時(shí)確定惫叛,VC中可設(shè)置)。
對(duì)于堆逞刷,頻繁的new/delete會(huì)造成大量?jī)?nèi)存碎片嘉涌,使程序效率降低。對(duì)于棧夸浅,它是先進(jìn)后出的隊(duì)列仑最,進(jìn)出一一對(duì)應(yīng),不產(chǎn)生碎片帆喇,運(yùn)行效率穩(wěn)定高警医。
所以我們可以得出結(jié)論:
1.局部變量的基本數(shù)據(jù)類型和引用存儲(chǔ)于棧中,引用的對(duì)象實(shí)體存儲(chǔ)于堆中坯钦。因?yàn)樗鼈儗儆诜椒ㄖ械淖兞吭せ剩芷陔S方法而結(jié)束。
2.成員變量全部存儲(chǔ)與堆中(包括基本數(shù)據(jù)類型婉刀,引用和引用的對(duì)象實(shí)體)吟温,因?yàn)樗鼈儗儆陬悾悓?duì)象終究是要被new出來使用的路星。
3.我們所說的內(nèi)存泄露溯街,只針對(duì)堆內(nèi)存诱桂,他們存放的就是引用指向的對(duì)象實(shí)體洋丐。
內(nèi)存泄露產(chǎn)生的原因
在Java中呈昔,內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是由垃圾收集器(Garbage Collection友绝,GC)完成的堤尾,程序員不需要通過調(diào)用函數(shù)來釋放內(nèi)存,但它只能回收無(wú)用并且不再被其它對(duì)象引用的那些對(duì)象所占用的空間迁客。
Java的內(nèi)存垃圾回收機(jī)制是從程序的主要運(yùn)行對(duì)象(如靜態(tài)對(duì)象/寄存器/棧上指向的堆內(nèi)存對(duì)象等)開始檢查引用鏈郭宝,當(dāng)遍歷一遍后得到上述這些無(wú)法回收的對(duì)象和他們所引用的對(duì)象鏈,組成無(wú)法回收的對(duì)象集合掷漱,而其他孤立對(duì)象(集)就作為垃圾回收粘室。GC為了能夠正確釋放對(duì)象,必須監(jiān)控每一個(gè)對(duì)象的運(yùn)行狀態(tài)卜范,包括對(duì)象的申請(qǐng)衔统、引用、被引用海雪、賦值等锦爵,GC都需要進(jìn)行監(jiān)控。監(jiān)視對(duì)象狀態(tài)是為了更加準(zhǔn)確地奥裸、及時(shí)地釋放對(duì)象险掀,而釋放對(duì)象的根本原則就是該對(duì)象不再被引用。
在Java中湾宙,這些無(wú)用的對(duì)象都由GC負(fù)責(zé)回收樟氢,因此程序員不需要考慮這部分的內(nèi)存泄露。雖然侠鳄,我們有幾個(gè)函數(shù)可以訪問GC嗡害,例如運(yùn)行GC的函數(shù)System.gc(),但是根據(jù)Java語(yǔ)言規(guī)范定義畦攘,該函數(shù)不保證JVM的垃圾收集器一定會(huì)執(zhí)行霸妹。因?yàn)椴煌腏VM實(shí)現(xiàn)者可能使用不同的算法管理GC。通常GC的線程的優(yōu)先級(jí)別較低知押。JVM調(diào)用GC的策略也有很多種叹螟,有的是內(nèi)存使用到達(dá)一定程度時(shí),GC才開始工作台盯,也有定時(shí)執(zhí)行的罢绽,有的是平緩執(zhí)行GC,有的是中斷式執(zhí)行GC静盅。但通常來說良价,我們不需要關(guān)心這些寝殴。
但是我們?nèi)匀豢梢匀ケO(jiān)聽系統(tǒng)的GC過程,以此來分析我們應(yīng)用程序當(dāng)前的內(nèi)存狀態(tài)明垢。那么怎樣才能去監(jiān)聽系統(tǒng)的GC過程呢蚣常?其實(shí)非常簡(jiǎn)單,系統(tǒng)每進(jìn)行一次GC操作時(shí)痊银,都會(huì)在LogCat中打印一條日志抵蚊,我們只要去分析這條日志就可以了,日志的基本格式如下所示:
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <Pause_time>
首先第一部分GC_Reason溯革,這個(gè)是觸發(fā)這次GC操作的原因贞绳,一般情況下一共有以下幾種觸發(fā)GC操作的原因:
GC_CONCURRENT: 當(dāng)我們應(yīng)用程序的堆內(nèi)存快要滿的時(shí)候,系統(tǒng)會(huì)自動(dòng)觸發(fā)GC操作來釋放內(nèi)存致稀。
GC_FOR_MALLOC: 當(dāng)我們的應(yīng)用程序需要分配更多內(nèi)存冈闭,可是現(xiàn)有內(nèi)存已經(jīng)不足的時(shí)候,系統(tǒng)會(huì)進(jìn)行GC操作來釋放內(nèi)存抖单。
GC_HPROF_DUMP_HEAP: 當(dāng)生成HPROF文件的時(shí)候萎攒,系統(tǒng)會(huì)進(jìn)行GC操作,關(guān)于HPROF文件我們下面會(huì)講到臭猜。
GC_EXPLICIT: 這種情況就是我們剛才提到過的躺酒,主動(dòng)通知系統(tǒng)去進(jìn)行GC操作,比如調(diào)用System.gc()方法來通知系統(tǒng)蔑歌「Γ或者在DDMS中,通過工具按鈕也是可以顯式地告訴系統(tǒng)進(jìn)行GC操作的次屠。
接下來第二部分Amount_freed园匹,表示系統(tǒng)通過這次GC操作釋放了多少內(nèi)存。
然后Heap_stats中會(huì)顯示當(dāng)前內(nèi)存的空閑比例以及使用情況(活動(dòng)對(duì)象所占內(nèi)存 / 當(dāng)前程序總內(nèi)存)劫灶。
最后Pause_time表示這次GC操作導(dǎo)致應(yīng)用程序暫停的時(shí)間裸违。關(guān)于這個(gè)暫停的時(shí)間,Android在2.3的版本當(dāng)中進(jìn)行過一次優(yōu)化本昏,在2.3之前GC操作是不能并發(fā)進(jìn)行的供汛,也就是系統(tǒng)正在進(jìn)行GC,那么應(yīng)用程序就只能阻塞住等待GC結(jié)束涌穆。雖說這個(gè)阻塞的過程并不會(huì)很長(zhǎng)怔昨,也就是幾百毫秒,但是用戶在使用我們的程序時(shí)還是有可能會(huì)感覺到略微的卡頓宿稀。而自2.3之后趁舀,GC操作改成了并發(fā)的方式進(jìn)行,就是說GC的過程中不會(huì)影響到應(yīng)用程序的正常運(yùn)行祝沸,但是在GC操作的開始和結(jié)束的時(shí)候會(huì)短暫阻塞一段時(shí)間矮烹,不過優(yōu)化到這種程度越庇,用戶已經(jīng)是完全無(wú)法察覺到了。
我們來看看Java中需要被回收的垃圾:
{
Person p1 = new Person();
……
}
引用句柄p1的作用域是從定義到“}”處奉狈,執(zhí)行完這對(duì)大括號(hào)中的所有代碼后卤唉,產(chǎn)生的Person對(duì)象就會(huì)變成垃圾,因?yàn)橐眠@個(gè)對(duì)象的句柄p1已超過其作用域嘹吨,p1失效搬味,在棧中被銷毀境氢,因此堆上的Person對(duì)象不再被任何句柄引用了蟀拷。 因此person變?yōu)槔瑫?huì)被回收萍聊。
這里我們需要講述一個(gè)關(guān)鍵詞:引用问芬,通過A能調(diào)用并訪問到B,那就說明A持有B的引用寿桨,或A就是B的引用此衅,B的引用計(jì)數(shù)+1.
(1)比如 Person p1 = new Person();通過P1能操作Person對(duì)象,因此P1是Person的引用亭螟;
(2)比如類O中有一個(gè)成員變量是I類對(duì)象挡鞍,因此我們可以使用o.i的方式來訪問I類對(duì)象的成員,因此o持有一個(gè)i對(duì)象的引用预烙。
GC過程與對(duì)象的引用類型是嚴(yán)重相關(guān)的墨微,我們來看看Java對(duì)引用的分類Strong reference, SoftReference, WeakReference, PhatomReference
在Android應(yīng)用的開發(fā)中,為了防止內(nèi)存溢出扁掸,在處理一些占用內(nèi)存大而且聲明周期較長(zhǎng)的對(duì)象時(shí)候翘县,可以盡量應(yīng)用軟引用和弱引用技術(shù)。
軟/弱引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用谴分,如果軟引用所引用的對(duì)象被垃圾回收器回收锈麸,Java虛擬機(jī)就會(huì)把這個(gè)軟引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。利用這個(gè)隊(duì)列可以得知被回收的軟/弱引用的對(duì)象列表牺蹄,從而為緩沖器清除已失效的軟/弱引用忘伞。
假設(shè)我們的應(yīng)用會(huì)用到大量的默認(rèn)圖片,比如應(yīng)用中有默認(rèn)的頭像沙兰,默認(rèn)游戲圖標(biāo)等等氓奈,這些圖片很多地方會(huì)用到。如果每次都去讀取圖片僧凰,由于讀取文件需要硬件操作探颈,速度較慢,會(huì)導(dǎo)致性能較低训措。所以我們考慮將圖片緩存起來伪节,需要的時(shí)候直接從內(nèi)存中讀取光羞。但是,由于圖片占用內(nèi)存空間比較大怀大,緩存很多圖片需要很多的內(nèi)存纱兑,就可能比較容易發(fā)生OutOfMemory異常。這時(shí)化借,我們可以考慮使用軟/弱引用技術(shù)來避免這個(gè)問題發(fā)生潜慎。以下就是高速緩沖器的雛形:
首先定義一個(gè)HashMap,保存軟引用對(duì)象蓖康。
1.private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
再來定義一個(gè)方法铐炫,保存Bitmap的軟引用到HashMap
public class CacheSoftRef {
//首先定義一個(gè)HashMap,保存引用對(duì)象
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
//再來定義一個(gè)方法,保存Bitmap的軟引用到HashMap
public void addBitmapToCache(String path) {
//強(qiáng)引用的Bitmap對(duì)象
Bitmap bitmap = BitmapFactory.decodeFile(path);
//軟引用的Bitmap對(duì)象
SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
//添加該對(duì)象到Map中使其緩存
imageCache.put(path, softBitmap);
}
//獲取的時(shí)候蒜焊,可以通過SoftReference的get()方法得到Bitmap對(duì)象
public Bitmap getBitmapByPath(String path) {
//從緩存中取軟引用的Bitmap對(duì)象
SoftReference<Bitmap> softBitmap = imageCache.get(path);
//判斷是否存在軟引用
if (softBitmap == null) {
return null;
}
//通過軟引用取出Bitmap對(duì)象倒信,如果由于內(nèi)存不足Bitmap被回收,將取得空泳梆,如果未被回收鳖悠,
//則可重復(fù)使用,提高速度优妙。
Bitmap bitmap = softBitmap.get();
return bitmap;
}
}
使用軟引用以后乘综,在OutOfMemory異常發(fā)生之前,這些緩存的圖片資源的內(nèi)存空間可以被釋放掉的套硼,從而避免內(nèi)存達(dá)到上限卡辰,避免Crash發(fā)生。
如果只是想避免OutOfMemory異常的發(fā)生熟菲,則可以使用軟引用看政。如果對(duì)于應(yīng)用的性能更在意,想盡快回收一些占用內(nèi)存比較大的對(duì)象抄罕,則可以使用弱引用允蚣。
另外可以根據(jù)對(duì)象是否經(jīng)常使用來判斷選擇軟引用還是弱引用。如果該對(duì)象可能會(huì)經(jīng)常使用的呆贿,就盡量用軟引用嚷兔。如果該對(duì)象不被使用的可能性更大些,就可以用弱引用做入。
所以我們得出內(nèi)存泄漏的原因:堆內(nèi)存中的長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象的強(qiáng)/軟引用冒晰,盡管短生命周期對(duì)象已經(jīng)不再需要,但是因?yàn)殚L(zhǎng)生命周期對(duì)象持有它的引用而導(dǎo)致不能被回收竟块,這就是Java中內(nèi)存泄露的根本原因壶运。
內(nèi)存泄漏的檢測(cè)
說了那么多關(guān)于內(nèi)存分配的知識(shí),接下來我們就看看Android給我們提供了哪些工具來解決內(nèi)存泄漏的問題
Allocation Tracker(Device Monitor)
Allocation Tracker位于Android Device Monitor中
Allocation Tracker面板
各名稱的含義如下:
Allocation Tracker操作
1.首先進(jìn)入你要追蹤的界面
2.點(diǎn)擊Start Tracking按鈕浪秘,開始跟蹤內(nèi)存分配軌跡
3.操作你的界面蒋情,盡量時(shí)間短點(diǎn)
4.點(diǎn)擊Get Allocations按鈕埠况,抓去內(nèi)存分配軌跡信息,顯示在右邊的面板中棵癣,默認(rèn)以內(nèi)存大小排序辕翰,你可以以分配順序排序或者仍以列排序。
5.logcat中會(huì)顯示出這次的軌跡共抓到內(nèi)存分配軌跡記錄數(shù)狈谊,可以簡(jiǎn)單的理解分配了多少次內(nèi)存喜命,這個(gè)數(shù)值和Alloc order的最大值是相等的
6.如果你不想看那么多亂七八糟的,你可以使用Filter來過濾河劝,輸入包名就可以了壁榕。
跟蹤內(nèi)存軌跡
如果這個(gè)時(shí)候我們想單獨(dú)獲取某次操作的內(nèi)存軌跡,首先一定要記得Stop Tracking再Start Tracking一下丧裁,讓追蹤點(diǎn)初始化一下护桦,然后就進(jìn)行我們需要觀察內(nèi)存變化的操作含衔,然后點(diǎn)擊Get Allocations煎娇,這個(gè)時(shí)候我們從首頁(yè)進(jìn)入一個(gè)詳情頁(yè),看一下我們的內(nèi)存分配軌跡:
追蹤到的內(nèi)存分配20293次,看著是不是有點(diǎn)無(wú)從下手贪染,沒關(guān)系缓呛,用Filter過濾下:
過濾后,就剩下了跟我們App源碼有關(guān)系的分配軌跡杭隙,我們隨便選擇一欄哟绊,可以看到其trace信息:
上圖中,我們可以看出來痰憎,在第635次內(nèi)存分配中票髓,分配的是IntroduceFragment對(duì)象,占用內(nèi)存224字節(jié)铣耘,處理線程Id為3245洽沟,在java.lang.Class的newInstance方法中分配的。從trace信息可以看出來該方法一步一步被調(diào)用的信息蜗细。
DDMS的Heap Viewer
Heap Viewer能做什么裆操?
?實(shí)時(shí)查看App分配的內(nèi)存大小和空閑內(nèi)存大小
?發(fā)現(xiàn)Memory Leaks
在Devices 中,點(diǎn)擊要監(jiān)控的程序炉媒。
點(diǎn)擊Devices視圖界面中最上方一排圖標(biāo)中的“Update Heap”
點(diǎn)擊Heap視圖
點(diǎn)擊Heap視圖中的“Cause GC”按鈕
到此為止需檢測(cè)的進(jìn)程就可以被監(jiān)視踪区。
按上圖的標(biāo)記順序按下,我們就能看到內(nèi)存的具體數(shù)據(jù)吊骤,右邊面板中數(shù)值會(huì)在每次GC時(shí)發(fā)生改變缎岗,包括App自動(dòng)觸發(fā)或者你來手動(dòng)觸發(fā)。
ok白粉,現(xiàn)在來解釋下面板中的名詞
總覽
詳情
下面是每一個(gè)對(duì)象都有的列名含義:
當(dāng)我們點(diǎn)擊某一行時(shí)传泊,可以看到如下的柱狀圖:
橫坐標(biāo)是對(duì)象的內(nèi)存大小茅郎,這些值隨著不同對(duì)象是不同的,縱坐標(biāo)是在某個(gè)內(nèi)存大小上的對(duì)象的數(shù)量
Heap Viewer的使用
我們說Heap Viewer適合發(fā)現(xiàn)內(nèi)存泄漏的問題或渤,那么如何檢測(cè)呢系冗?
那么如何檢測(cè)呢?
進(jìn)入某應(yīng)用薪鹦,不斷的操作該應(yīng)用掌敬,同時(shí)注意觀察data object的Total Size值,正常情況下Total Size值都會(huì)穩(wěn)定在一個(gè)有限的范圍內(nèi)池磁,也就是說由于程序中的的代碼良好奔害,沒有造成對(duì)象不被垃圾回收的情況。
所以說雖然我們不斷的操作會(huì)不斷的生成很多對(duì)象地熄,而在虛擬機(jī)不斷的進(jìn)行GC的過程中华临,這些對(duì)象都被回收了,內(nèi)存占用量會(huì)會(huì)落到一個(gè)穩(wěn)定的水平端考;反之如果代碼中存在沒有釋放對(duì)象引用的情況雅潭,則data object的Total Size值在每次GC后不會(huì)有明顯的回落。隨著操作次數(shù)的增多Total Size的值會(huì)越來越大却特,直到到達(dá)一個(gè)上限后導(dǎo)致進(jìn)程被殺掉扶供。
MAT工具
那么通過上面DDMS工具,現(xiàn)在我們已經(jīng)可以比較輕松地發(fā)現(xiàn)應(yīng)用程序中是否存在內(nèi)存泄露的現(xiàn)象了裂明。
我們應(yīng)該怎么定位到具體是哪里出的內(nèi)存泄露呢椿浓?這就需要借助一個(gè)內(nèi)存分析工具了,叫做Eclipse Memory Analyzer(MAT)闽晦。
為了使用該工具扳碍,我們需要hprof文件。但是該文件不能直接被MAT使用仙蛉,需要進(jìn)行一步轉(zhuǎn)化笋敞,可以使用hprof-conv命令來轉(zhuǎn)化,但是Android Studio可以直接轉(zhuǎn)化,轉(zhuǎn)化方法如下:
1.選擇一個(gè)hprof文件捅儒,點(diǎn)擊右鍵選擇Export to standard .hprof選項(xiàng)液样。
2.填寫更改后的文件名和路徑:
點(diǎn)擊OK按鈕后,MAT工具所需的文件就生成了巧还,下面我們用MAT來打開該工具:
1.打開MAT后選擇File->Open File選擇我們剛才生成的doctorq.hprof文件
2.選擇該文件后鞭莽,MAT會(huì)有幾秒種的時(shí)間解析該文件,有的hprof文件可能過大麸祷,會(huì)有更長(zhǎng)的時(shí)間解析澎怒,解析后,展現(xiàn)在我們的面前的界面如下:
上圖最中央的那個(gè)餅狀圖展示了最大的幾個(gè)對(duì)象所占內(nèi)存的比例,這張圖中提供的內(nèi)容并不多喷面,我們可以忽略它星瘾。在這個(gè)餅狀圖的下方就有幾個(gè)非常有用的工具了,我們來學(xué)習(xí)一下惧辈。
Histogram可以列出內(nèi)存中每個(gè)對(duì)象的名字琳状、數(shù)量以及大小。
Dominator Tree會(huì)將所有內(nèi)存中的對(duì)象按大小進(jìn)行排序盒齿,并且我們可以分析對(duì)象之間的引用結(jié)構(gòu)念逞。
現(xiàn)在點(diǎn)擊Dominator Tree,結(jié)果如下圖所示:
首先Retained Heap表示這個(gè)對(duì)象以及它所持有的其它引用(包括直接和間接)所占的總內(nèi)存边翁,因此從上圖中看翎承,前兩行的Retained Heap是最大的,我們分析內(nèi)存泄漏時(shí)符匾,內(nèi)存最大的對(duì)象也是最應(yīng)該去懷疑的叨咖。
在每一行的最左邊都有一個(gè)文件型的圖標(biāo),這些圖標(biāo)有的左下角帶有一個(gè)點(diǎn)啊胶,有的則沒有甸各。帶點(diǎn)的對(duì)象就表示是可以被GC Roots訪問到的,可以被GC Root訪問到的對(duì)象都是無(wú)法被回收的创淡。那么這就說明所有帶紅色的對(duì)象都是泄漏的對(duì)象嗎痴晦?當(dāng)然不是,因?yàn)橛行?duì)象系統(tǒng)需要一直使用琳彩,本來就不應(yīng)該被回收。我們可以注意到部凑,上圖當(dāng)中所有帶點(diǎn)的對(duì)象最右邊都有寫一個(gè)System Class露乏,說明這是一個(gè)由系統(tǒng)管理的對(duì)象,并不是由我們自己創(chuàng)建并導(dǎo)致內(nèi)存泄漏的對(duì)象涂邀。
上圖當(dāng)中瘟仿,除了帶有System Class的行之外,最大的就是第二行的Bitmap對(duì)象了比勉,雖然Bitmap對(duì)象現(xiàn)在不能被GC Roots訪問到劳较,但不代表著Bitmap所持有的其它引用也不會(huì)被GC Roots訪問到。現(xiàn)在我們可以對(duì)著第二行點(diǎn)擊右鍵 -> Path to GC Roots -> exclude weak references浩聋,為什么選擇exclude weak references呢观蜗?因?yàn)槿跻檬遣粫?huì)阻止對(duì)象被垃圾回收器回收的,所以我們這里直接把它排除掉
可以看到衣洁,Bitmap對(duì)象經(jīng)過層層引用之后墓捻,到了MainActivityLeakClass這個(gè)對(duì)象,然后在圖標(biāo)的左下角有個(gè)點(diǎn)坊夫,就說明在這里可以被GCRoots訪問到了砖第,并且這是由我們自己創(chuàng)建的Thread撤卢,并不是SystemClass了,那么由于MainActivityLeakClass這個(gè)對(duì)象梧兼,然后在圖標(biāo)的左下角有個(gè)點(diǎn)放吩,就說明在這里可以被GCRoots訪問到了,并且這是由我們自己創(chuàng)建的Thread羽杰,并不是SystemClass了屎慢,那么由于MainActivityLeakClass能被GC Roots訪問到導(dǎo)致不能被回收,導(dǎo)致它所持有的其它引用也無(wú)法被回收了忽洛,包括MainActivity腻惠,也包括MainActivity中所包含的圖片。
通過這種方式欲虚,我們就成功地將內(nèi)存泄漏的原因找出來了集灌。
Histogram的用法
用的最多的功能是 Histogram,點(diǎn)擊 Actions下的 Histogram項(xiàng)將得到 Histogram結(jié)果:
它按類名將所有的實(shí)例對(duì)象列出來,可以點(diǎn)擊表頭進(jìn)行排序,在表的第一行可以輸入正則表達(dá)式來匹配結(jié)果 :
在某一項(xiàng)上右鍵打開菜單選擇 list objects ->with incoming refs 將列出該類的實(shí)例:
它展示了對(duì)象間的引用關(guān)系复哆,比如展開后的第一個(gè)子項(xiàng)表示這個(gè) HomePage(0x420ca5b0)被HomePageContainer(0x420c9e40)中的 mHomePage屬性所引用.
快速找出某個(gè)實(shí)例沒被釋放的原因欣喧,可以右健 Path to GC Roots–>exclue all phantom/weak/soft etc. reference :
得到的結(jié)果是:
從表中可以看出 PreferenceManager -> … ->HomePage這條線路就引用著這個(gè) HomePage實(shí)例。用這個(gè)方法可以快速找到某個(gè)對(duì)象的 GC Root,一個(gè)存在 GC Root的對(duì)象是不會(huì)被 GC回收掉的.
Histogram 對(duì)比
為查找內(nèi)存泄漏梯找,通常需要兩個(gè) Dump結(jié)果作對(duì)比唆阿,打開 Navigator History面板,將兩個(gè)表的 Histogram結(jié)果都添加到 Compare Basket中去 :
添加好后锈锤,打開 Compare Basket面板掂骏,得到結(jié)果:
點(diǎn)擊右上角的 ! 按鈕假栓,將得到比對(duì)結(jié)果:
注意,上面這個(gè)對(duì)比結(jié)果不利于查找差異,可以調(diào)整對(duì)比選項(xiàng):
再把對(duì)比的結(jié)果排序芒划,就可得到直觀的對(duì)比結(jié)果:
也可以對(duì)比兩個(gè)對(duì)象集合只锭,方法與此類似闭专,都是將兩個(gè) Dump結(jié)果中的對(duì)象集合添加到Compare Basket中去對(duì)比拨扶。找出差異后用 Histogram查詢的方法找出 GC Root,定位到具體的某個(gè)對(duì)象上呼巴。
LeakCanary
有別于MAT和AndroidStudio中Monitors的實(shí)時(shí)內(nèi)存占用圖泽腮,使用LeakCanary分析內(nèi)存泄露就簡(jiǎn)單多了LeakCanary是Square開源了一個(gè)內(nèi)存泄露自動(dòng)探測(cè)神器 。這是項(xiàng)目的github倉(cāng)庫(kù)地址:https://github.com/square/leakcanary 衣赶。使用非常簡(jiǎn)單诊赊,在build.gradle中引入包依賴
debugCompile 'com.squareup.leakcanary:leakcanary-
android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-
android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-
android-no-op:1.5'
在Application中的onCreate方法中增加初始化代碼:
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for
// heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
集成后什么都不用做,按照正常測(cè)試屑埋,當(dāng)有內(nèi)存泄漏發(fā)生后豪筝,應(yīng)用會(huì)通過系統(tǒng)通知欄發(fā)出通知,點(diǎn)擊通知就可以進(jìn)入查看內(nèi)存泄漏的具體信息。在這里舉個(gè)實(shí)踐中的例子续崖。把LeakCanary集成到項(xiàng)目中后敲街,等App啟動(dòng)后一會(huì),系統(tǒng)通知到了严望,點(diǎn)擊通知多艇,跳轉(zhuǎn)到泄漏的詳情頁(yè)面進(jìn)行查看:很明顯,WebSiteQueryActivity泄露了像吻。首先峻黍,static 的MaskHeadView.fLayout變量引用了FrameLayout.mContext對(duì)象,這個(gè)對(duì)象的引用就是指向了WebSiteQueryActivity的實(shí)例拨匆,導(dǎo)致了它的泄漏姆涩,在第二節(jié)中我們說過static對(duì)象是內(nèi)部的static對(duì)象是比較容易造成內(nèi)存泄漏的,檢查代碼發(fā)現(xiàn)惭每,MaskHeadView直接在WebSiteQueryActivity的xml文件中使用了骨饿,因此持有WebSiteQueryActivity的實(shí)例,因?yàn)閒Layout對(duì)象是靜態(tài)的台腥,因此它的生命周期與Application同樣長(zhǎng)宏赘,因此WebSiteQueryActivity退出后,它的實(shí)例引用依然被fLayout持有黎侈,導(dǎo)致它無(wú)法被回收從而內(nèi)存泄露了察署。仔細(xì)檢查代碼,發(fā)現(xiàn)fLayout并沒有被外部使用到峻汉,應(yīng)該是之前的開發(fā)者手抖加了個(gè)static字段上去或者是現(xiàn)在不用了贴汪,但是沒有去掉,在這里我直接去掉了這個(gè)修飾符俱济,在此build代碼嘶是,這個(gè)內(nèi)存泄漏的現(xiàn)象消失了。
/去掉static修飾符蛛碌,避免static對(duì)象引起的內(nèi)存泄漏
private static FrameLayout fLayout;
public MaskHeadView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context=context;
initView(context);
}
private void initView(Context context2) {
view = LayoutInflater.from(context).inflate(R.layout.
mask_head_view, this);
fLayout=(FrameLayout) view.findViewById(R.id.
mask_container);
}
這只是個(gè)極簡(jiǎn)單的例子,但方法是一樣的辖源。順便提一句蔚携,其實(shí)無(wú)論是MAT工具的內(nèi)存分析,還是AndroidStudio中自帶的分析工具亦或是LeakCanary克饶,原理都是一樣的酝蜒,都是dump java heap出來進(jìn)行分析,找到泄漏的問題矾湃,只是LeakCanary幫我們把分析的工作做了亡脑。但值得一提的是,LeakCanary并不是萬(wàn)能的,有些內(nèi)存泄漏霉咨,它也無(wú)法檢測(cè)出來蛙紫。
好了,關(guān)于內(nèi)存泄露的相關(guān)內(nèi)容就介紹到這途戒,后期將持續(xù)更新坑傅,可以關(guān)注一下查看后續(xù)。
本文轉(zhuǎn)自csdn一位博主[伯努力不努力]