Part 1
在長(zhǎng)久以來的 Android 開發(fā)過程中南用,內(nèi)存泄漏一直是一個(gè)比較頭疼的問題熙卡。內(nèi)存泄漏會(huì)導(dǎo)致應(yīng)用卡頓洲脂,用戶體驗(yàn)不佳斤儿,甚至?xí)斐蓱?yīng)用崩潰的嚴(yán)重后果。所以如何科學(xué)地進(jìn)行內(nèi)存管理一直是大家探討的話題恐锦,從一開始主動(dòng)使用 MAT 分析 hprof 文件往果,到后來 LeakCanary “被動(dòng)”的接收內(nèi)存泄漏消息。應(yīng)用中發(fā)現(xiàn)內(nèi)存泄漏的手段越來越多了一铅,操作也越來越便捷陕贮,但內(nèi)存泄漏的問題還是不能輕易忽視的,提高應(yīng)用的體驗(yàn)和質(zhì)量也是迫在眉睫潘飘。
那今天肮之,就從最基本的開始聊聊內(nèi)存泄漏。
Part 2
內(nèi)存泄漏簡(jiǎn)單粗俗的講卜录,就是該被釋放的對(duì)象沒有釋放戈擒,一直被某個(gè)或某些實(shí)例所持有卻不再被使用導(dǎo)致 GC 不能回收。我們所說的內(nèi)存泄露是針對(duì)于堆內(nèi)存而言艰毒,堆內(nèi)存中存放的就是引用指向的對(duì)象實(shí)體筐高。
在這里先科普下內(nèi)存分配的三種策略。(以下這段講解來自于 《內(nèi)存泄露從入門到精通三部曲之基礎(chǔ)知識(shí)篇》)
- 靜態(tài)的现喳,使用的內(nèi)存空間是靜態(tài)存儲(chǔ)區(qū)
- 棧式的凯傲,使用的內(nèi)存空間是棧區(qū)
- 堆式的,使用的內(nèi)存空間是堆區(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)定高讶泰。
說了這么多了骡湖,我們來看一個(gè)例子吧:
class Student {
private int age = 10;
private School school = new School();
public void doHomework() {
Book book = new Book();
int pageNo = 15;
}
}
Student s = new Student();
s 自己存放在棧中,而 s 指向的對(duì)象實(shí)體存放在堆中峻厚;
其中 s 這個(gè)對(duì)象實(shí)體中的全局變量 age 和 school 都是存放在堆中(包括基本數(shù)據(jù)類型响蕴、引用和引用的對(duì)象實(shí)體)
doHomework 中的引用變量 book 和局部變量 pageNo 是存放在棧中的,而引用變量 book 指向的對(duì)象是存放在堆中的惠桃。
結(jié)論:(以下結(jié)論來自于《Android 內(nèi)存泄漏探討》)
局部變量的基本數(shù)據(jù)類型和引用存儲(chǔ)于棧中浦夷,引用的對(duì)象實(shí)體存儲(chǔ)于堆中」纪酰—— 因?yàn)樗鼈儗儆诜椒ㄖ械淖兞颗芷陔S方法而結(jié)束。
成員變量全部存儲(chǔ)與堆中(包括基本數(shù)據(jù)類型呐馆,引用和引用的對(duì)象實(shí)體)—— 因?yàn)樗鼈儗儆陬惙实蓿悓?duì)象終究是要被new出來使用的。
Part 3
那么有沒有想過汹来,內(nèi)存為什么會(huì)泄露续膳?
Java的內(nèi)存垃圾回收機(jī)制是從程序的主要運(yùn)行對(duì)象(如靜態(tài)對(duì)象/寄存器/棧上指向的堆內(nèi)存對(duì)象等)開始檢查引用鏈,當(dāng)遍歷一遍后得到上述這些無法回收的對(duì)象和他們所引用的對(duì)象鏈收班,組成無法回收的對(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中熙兔,這些無用的對(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)心這些。
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ù)胰挑。
如果只是想避免OutOfMemory異常的發(fā)生,則可以使用軟引用椿肩。如果對(duì)于應(yīng)用的性能更在意瞻颂,想盡快回收一些占用內(nèi)存比較大的對(duì)象,則可以使用弱引用覆旱。
另外可以根據(jù)對(duì)象是否經(jīng)常使用來判斷選擇軟引用還是弱引用蘸朋。如果該對(duì)象可能會(huì)經(jīng)常使用的,就盡量用軟引用扣唱。如果該對(duì)象不被使用的可能性更大些,就可以用弱引用团南。
結(jié)論:
堆內(nèi)存中的長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象的強(qiáng)/軟引用噪沙,盡管短生命周期對(duì)象已經(jīng)不再需要,但是因?yàn)殚L(zhǎng)生命周期對(duì)象持有它的引用而導(dǎo)致不能被回收吐根,這就是Java中內(nèi)存泄露的根本原因正歼。
Part 4
Android中常見的內(nèi)存泄漏問題:
- 單例造成的內(nèi)存泄露
- InnerClass匿名內(nèi)部類
- Activity Context 的不正確使用
- Handler引起的內(nèi)存泄漏
- 注冊(cè)監(jiān)聽器的泄漏
- Cursor,Stream沒有close拷橘,View沒有recyle
- 集合中對(duì)象沒清理造成的內(nèi)存泄漏
- WebView造成的泄露
- 構(gòu)造Adapter時(shí)局义,沒有使用緩存的ConvertView
具體可以參考 Android內(nèi)存泄漏分析心得
Part 5
Android 中檢測(cè)內(nèi)存泄漏的工具
- MAT
- Android Profiler
- LeakCanary
Part 6
參考資料