Android 內(nèi)存泄漏總結(jié)
內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題搪桂。內(nèi)存泄漏大家都不陌生了,簡(jiǎn)單粗俗的講域滥,就是該被釋放的對(duì)象沒有釋放坟奥,一直被某個(gè)或某些實(shí)例所持有卻不再被使用導(dǎo)致 GC 不能回收明刷。最近自己閱讀了大量相關(guān)的文檔資料婴栽,打算做個(gè) 總結(jié) 沉淀下來跟大家一起分享和學(xué)習(xí),也給自己一個(gè)警示辈末,以后 coding 時(shí)怎么避免這些情況愚争,提高應(yīng)用的體驗(yàn)和質(zhì)量。
我會(huì)從 java 內(nèi)存泄漏的基礎(chǔ)知識(shí)開始挤聘,并通過具體例子來說明 Android 引起內(nèi)存泄漏的各種原因轰枝,以及如何利用工具來分析應(yīng)用內(nèi)存泄漏,最后再做總結(jié)组去。
Java 內(nèi)存分配策略
Java 程序運(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ū)):主要存放靜態(tài)數(shù)據(jù)、全局 static 數(shù)據(jù)和常量键闺。這塊內(nèi)存在程序編譯時(shí)就已經(jīng)分配好寿烟,并且在程序整個(gè)運(yùn)行期間都存在。
棧區(qū) :當(dāng)方法被執(zhí)行時(shí)辛燥,方法體內(nèi)的局部變量(其中包括基礎(chǔ)數(shù)據(jù)類型筛武、對(duì)象的引用)都在棧上創(chuàng)建缝其,并在方法執(zhí)行結(jié)束時(shí)這些局部變量所持有的內(nèi)存將會(huì)自動(dòng)被釋放。因?yàn)闂?nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中徘六,效率很高内边,但是分配的內(nèi)存容量有限。
堆區(qū) : 又稱動(dòng)態(tài)內(nèi)存分配硕噩,通常就是指在程序運(yùn)行時(shí)直接 new 出來的內(nèi)存假残,也就是對(duì)象的實(shí)例。這部分內(nèi)存在不使用時(shí)將會(huì)由 Java 垃圾回收器來負(fù)責(zé)回收炉擅。
棧與堆的區(qū)別:
在方法體內(nèi)定義的(局部變量)一些基本類型的變量和對(duì)象的引用變量都是在方法的棧內(nèi)存中分配的辉懒。當(dāng)在一段方法塊中定義一個(gè)變量時(shí),Java 就會(huì)在棧中為該變量分配內(nèi)存空間谍失,當(dāng)超過該變量的作用域后眶俩,該變量也就無效了,分配給它的內(nèi)存空間也將被釋放掉快鱼,該內(nèi)存空間可以被重新使用颠印。
堆內(nèi)存用來存放所有由 new 創(chuàng)建的對(duì)象(包括該對(duì)象其中的所有成員變量)和數(shù)組。在堆中分配的內(nèi)存抹竹,將由 Java 垃圾回收器來自動(dòng)管理线罕。在堆中產(chǎn)生了一個(gè)數(shù)組或者對(duì)象后,還可以在棧中定義一個(gè)特殊的變量窃判,這個(gè)變量的取值等于數(shù)組或者對(duì)象在堆內(nèi)存中的首地址钞楼,這個(gè)特殊的變量就是我們上面說的引用變量。我們可以通過這個(gè)引用變量來訪問堆中的對(duì)象或者數(shù)組袄琳。
舉個(gè)例子:
public class Sample {
int s1 = 0;
Sample mSample1 = new Sample();
public void method() {
int s2 = 1;
Sample mSample2 = new Sample();
}
}
Sample mSample3 = new Sample();
Sample 類的局部變量 s2 和引用變量 mSample2 都是存在于棧中询件,但 mSample2 指向的對(duì)象是存在于堆上的。
mSample3 指向的對(duì)象實(shí)體存放在堆上唆樊,包括這個(gè)對(duì)象的所有成員變量 s1 和 mSample1宛琅,而它自己存在于棧中。
結(jié)論:
局部變量的基本數(shù)據(jù)類型和引用存儲(chǔ)于棧中逗旁,引用的對(duì)象實(shí)體存儲(chǔ)于堆中嘿辟。—— 因?yàn)樗鼈儗儆诜椒ㄖ械淖兞科В芷陔S方法而結(jié)束仓洼。
成員變量全部存儲(chǔ)與堆中(包括基本數(shù)據(jù)類型,引用和引用的對(duì)象實(shí)體)—— 因?yàn)樗鼈儗儆陬惖淌妫悓?duì)象終究是要被new出來使用的色建。
了解了 Java 的內(nèi)存分配之后,我們?cè)賮砜纯?Java 是怎么管理內(nèi)存的舌缤。
Java是如何管理內(nèi)存
Java的內(nèi)存管理就是對(duì)象的分配和釋放問題箕戳。在 Java 中某残,程序員需要通過關(guān)鍵字 new 為每個(gè)對(duì)象申請(qǐng)內(nèi)存空間 (基本類型除外),所有的對(duì)象都在堆 (Heap)中分配空間陵吸。另外玻墅,對(duì)象的釋放是由 GC 決定和執(zhí)行的。在 Java 中壮虫,內(nèi)存的分配是由程序完成的澳厢,而內(nèi)存的釋放是由 GC 完成的,這種收支兩條線的方法確實(shí)簡(jiǎn)化了程序員的工作囚似。但同時(shí)剩拢,它也加重了JVM的工作。這也是 Java 程序運(yùn)行速度較慢的原因之一饶唤。因?yàn)樾旆ィ珿C 為了能夠正確釋放對(duì)象,GC 必須監(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ì)象不再被引用。
為了更好理解 GC 的工作原理揭措,我們可以將對(duì)象考慮為有向圖的頂點(diǎn),將引用關(guān)系考慮為圖的有向邊刻蚯,有向邊從引用者指向被引對(duì)象绊含。另外,每個(gè)線程對(duì)象可以作為一個(gè)圖的起始頂點(diǎn)炊汹,例如大多程序從 main 進(jìn)程開始執(zhí)行躬充,那么該圖就是以 main 進(jìn)程頂點(diǎn)開始的一棵根樹。在這個(gè)有向圖中讨便,根頂點(diǎn)可達(dá)的對(duì)象都是有效對(duì)象充甚,GC將不回收這些對(duì)象。如果某個(gè)對(duì)象 (連通子圖)與這個(gè)根頂點(diǎn)不可達(dá)(注意霸褒,該圖為有向圖)伴找,那么我們認(rèn)為這個(gè)(這些)對(duì)象不再被引用,可以被 GC 回收废菱。
以下技矮,我們舉一個(gè)例子說明如何用有向圖表示內(nèi)存管理抖誉。對(duì)于程序的每一個(gè)時(shí)刻,我們都有一個(gè)有向圖表示JVM的內(nèi)存分配情況衰倦。以下右圖袒炉,就是左邊程序運(yùn)行到第6行的示意圖。
Java使用有向圖的方式進(jìn)行內(nèi)存管理樊零,可以消除引用循環(huán)的問題我磁,例如有三個(gè)對(duì)象,相互引用驻襟,只要它們和根進(jìn)程不可達(dá)的夺艰,那么GC也是可以回收它們的。這種方式的優(yōu)點(diǎn)是管理內(nèi)存的精度很高塑悼,但是效率較低劲适。另外一種常用的內(nèi)存管理技術(shù)是使用計(jì)數(shù)器,例如COM模型采用計(jì)數(shù)器方式管理構(gòu)件厢蒜,它與有向圖相比霞势,精度行低(很難處理循環(huán)引用的問題),但執(zhí)行效率很高斑鸦。
什么是Java中的內(nèi)存泄露
在Java中愕贡,內(nèi)存泄漏就是存在一些被分配的對(duì)象,這些對(duì)象有下面兩個(gè)特點(diǎn)巷屿,首先固以,這些對(duì)象是可達(dá)的,即在有向圖中嘱巾,存在通路可以與其相連憨琳;其次,這些對(duì)象是無用的旬昭,即程序以后不會(huì)再使用這些對(duì)象篙螟。如果對(duì)象滿足這兩個(gè)條件,這些對(duì)象就可以判定為Java中的內(nèi)存泄漏问拘,這些對(duì)象不會(huì)被GC所回收遍略,然而它卻占用內(nèi)存。
在C++中骤坐,內(nèi)存泄漏的范圍更大一些绪杏。有些對(duì)象被分配了內(nèi)存空間,然后卻不可達(dá)纽绍,由于C++中沒有GC蕾久,這些內(nèi)存將永遠(yuǎn)收不回來。在Java中拌夏,這些不可達(dá)的對(duì)象都由GC負(fù)責(zé)回收腔彰,因此程序員不需要考慮這部分的內(nèi)存泄露叫编。
通過分析,我們得知霹抛,對(duì)于C++搓逾,程序員需要自己管理邊和頂點(diǎn),而對(duì)于Java程序員只需要管理邊就可以了(不需要管理頂點(diǎn)的釋放)杯拐。通過這種方式霞篡,Java提高了編程的效率。
因此端逼,通過以上分析朗兵,我們知道在Java中也有內(nèi)存泄漏,但范圍比C++要小一些顶滩。因?yàn)镴ava從語言上保證余掖,任何對(duì)象都是可達(dá)的,所有的不可達(dá)對(duì)象都由GC管理礁鲁。
對(duì)于程序員來說盐欺,GC基本是透明的,不可見的仅醇。雖然冗美,我們只有幾個(gè)函數(shù)可以訪問GC,例如運(yùn)行GC的函數(shù)System.gc()析二,但是根據(jù)Java語言規(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)心這些手素。除非在一些特定的場(chǎng)合荒澡,GC的執(zhí)行影響應(yīng)用程序的性能猜绣,例如對(duì)于基于Web的實(shí)時(shí)系統(tǒng)囊蓝,如網(wǎng)絡(luò)游戲等,用戶不希望GC突然中斷應(yīng)用程序執(zhí)行而進(jìn)行垃圾回收拥知,那么我們需要調(diào)整GC的參數(shù)山卦,讓GC能夠通過平緩的方式釋放內(nèi)存,例如將垃圾回收分解為一系列的小步驟執(zhí)行氓辣,Sun提供的HotSpot JVM就支持這一特性秒裕。
同樣給出一個(gè) Java 內(nèi)存泄漏的典型例子,
Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
在這個(gè)例子中钞啸,我們循環(huán)申請(qǐng)Object對(duì)象几蜻,并將所申請(qǐng)的對(duì)象放入一個(gè) Vector 中,如果我們僅僅釋放引用本身体斩,那么 Vector 仍然引用該對(duì)象梭稚,所以這個(gè)對(duì)象對(duì) GC 來說是不可回收的。因此絮吵,如果對(duì)象加入到Vector 后弧烤,還必須從 Vector 中刪除,最簡(jiǎn)單的方法就是將 Vector 對(duì)象設(shè)置為 null蹬敲。
詳細(xì)Java中的內(nèi)存泄漏
1.Java內(nèi)存回收機(jī)制
不論哪種語言的內(nèi)存分配方式暇昂,都需要返回所分配內(nèi)存的真實(shí)地址,也就是返回一個(gè)指針到內(nèi)存塊的首地址粱栖。Java中對(duì)象是采用new或者反射的方法創(chuàng)建的话浇,這些對(duì)象的創(chuàng)建都是在堆(Heap)中分配的,所有對(duì)象的回收都是由Java虛擬機(jī)通過垃圾回收機(jī)制完成的闹究。GC為了能夠正確釋放對(duì)象幔崖,會(huì)監(jiān)控每個(gè)對(duì)象的運(yùn)行狀況,對(duì)他們的申請(qǐng)渣淤、引用赏寇、被引用、賦值等狀況進(jìn)行監(jiān)控价认,Java會(huì)使用有向圖的方法進(jìn)行管理內(nèi)存嗅定,實(shí)時(shí)監(jiān)控對(duì)象是否可以達(dá)到,如果不可到達(dá)用踩,則就將其回收渠退,這樣也可以消除引用循環(huán)的問題。在Java語言中脐彩,判斷一個(gè)內(nèi)存空間是否符合垃圾收集標(biāo)準(zhǔn)有兩個(gè):一個(gè)是給對(duì)象賦予了空值null碎乃,以下再?zèng)]有調(diào)用過,另一個(gè)是給對(duì)象賦予了新值惠奸,這樣重新分配了內(nèi)存空間梅誓。
2.Java內(nèi)存泄漏引起的原因
內(nèi)存泄漏是指無用對(duì)象(不再使用的對(duì)象)持續(xù)占有內(nèi)存或無用對(duì)象的內(nèi)存得不到及時(shí)釋放,從而造成內(nèi)存空間的浪費(fèi)稱為內(nèi)存泄漏。內(nèi)存泄露有時(shí)不嚴(yán)重且不易察覺梗掰,這樣開發(fā)者就不知道存在內(nèi)存泄露嵌言,但有時(shí)也會(huì)很嚴(yán)重,會(huì)提示你Out of memory及穗。j
Java內(nèi)存泄漏的根本原因是什么呢摧茴?長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象的引用就很可能發(fā)生內(nèi)存泄漏,盡管短生命周期對(duì)象已經(jīng)不再需要埂陆,但是因?yàn)殚L(zhǎng)生命周期持有它的引用而導(dǎo)致不能被回收蓬蝶,這就是Java中內(nèi)存泄漏的發(fā)生場(chǎng)景。具體主要有如下幾大類:
1猜惋、靜態(tài)集合類引起內(nèi)存泄漏:
像HashMap丸氛、Vector等的使用最容易出現(xiàn)內(nèi)存泄露,這些靜態(tài)變量的生命周期和應(yīng)用程序一致著摔,他們所引用的所有的對(duì)象Object也不能被釋放缓窜,因?yàn)樗麄円矊⒁恢北籚ector等引用著。
例如
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
在這個(gè)例子中谍咆,循環(huán)申請(qǐng)Object 對(duì)象禾锤,并將所申請(qǐng)的對(duì)象放入一個(gè)Vector 中,如果僅僅釋放引用本身(o=null)摹察,那么Vector 仍然引用該對(duì)象恩掷,所以這個(gè)對(duì)象對(duì)GC 來說是不可回收的。因此供嚎,如果對(duì)象加入到Vector 后黄娘,還必須從Vector 中刪除,最簡(jiǎn)單的方法就是將Vector對(duì)象設(shè)置為null克滴。
2逼争、當(dāng)集合里面的對(duì)象屬性被修改后,再調(diào)用remove()方法時(shí)不起作用劝赔。
例如:
public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孫悟空","pwd2",26);
Person p3 = new Person("豬八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("總共有:"+set.size()+" 個(gè)元素!"); //結(jié)果:總共有:3 個(gè)元素!
p3.setAge(2); //修改p3的年齡,此時(shí)p3元素對(duì)應(yīng)的hashcode值發(fā)生改變
set.remove(p3); //此時(shí)remove不掉誓焦,造成內(nèi)存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println("總共有:"+set.size()+" 個(gè)元素!"); //結(jié)果:總共有:4 個(gè)元素!
for (Person person : set)
{
System.out.println(person);
}
}
3着帽、監(jiān)聽器
在java 編程中杂伟,我們都需要和監(jiān)聽器打交道,通常一個(gè)應(yīng)用當(dāng)中會(huì)用到很多監(jiān)聽器仍翰,我們會(huì)調(diào)用一個(gè)控件的諸如addXXXListener()等方法來增加監(jiān)聽器赫粥,但往往在釋放對(duì)象的時(shí)候卻沒有記住去刪除這些監(jiān)聽器,從而增加了內(nèi)存泄漏的機(jī)會(huì)歉备。
4傅是、各種連接
比如數(shù)據(jù)庫連接(dataSourse.getConnection())匪燕,網(wǎng)絡(luò)連接(socket)和io連接蕾羊,除非其顯式的調(diào)用了其close()方法將其連接關(guān)閉喧笔,否則是不會(huì)自動(dòng)被GC 回收的。對(duì)于Resultset 和Statement 對(duì)象可以不進(jìn)行顯式回收龟再,但Connection 一定要顯式回收书闸,因?yàn)镃onnection 在任何時(shí)候都無法自動(dòng)回收,而Connection一旦回收利凑,Resultset 和Statement 對(duì)象就會(huì)立即為NULL浆劲。但是如果使用連接池,情況就不一樣了哀澈,除了要顯式地關(guān)閉連接牌借,還必須顯式地關(guān)閉Resultset Statement 對(duì)象(關(guān)閉其中一個(gè),另外一個(gè)也會(huì)關(guān)閉)割按,否則就會(huì)造成大量的Statement 對(duì)象無法釋放膨报,從而引起內(nèi)存泄漏。這種情況下一般都會(huì)在try里面去的連接适荣,在finally里面釋放連接现柠。
5、內(nèi)部類和外部模塊的引用
內(nèi)部類的引用是比較容易遺忘的一種弛矛,而且一旦沒釋放可能導(dǎo)致一系列的后繼類對(duì)象沒有釋放够吩。此外程序員還要小心外部模塊不經(jīng)意的引用,例如程序員A 負(fù)責(zé)A 模塊丈氓,調(diào)用了B 模塊的一個(gè)方法如:
public void registerMsg(Object b);
這種調(diào)用就要非常小心了周循,傳入了一個(gè)對(duì)象,很可能模塊B就保持了對(duì)該對(duì)象的引用万俗,這時(shí)候就需要注意模塊B 是否提供相應(yīng)的操作去除引用鱼鼓。
6、單例模式
不正確使用單例模式是引起內(nèi)存泄漏的一個(gè)常見問題该编,單例對(duì)象在初始化后將在JVM的整個(gè)生命周期中存在(以靜態(tài)變量的方式)迄本,如果單例對(duì)象持有外部的引用,那么這個(gè)對(duì)象將不能被JVM正晨慰ⅲ回收嘉赎,導(dǎo)致內(nèi)存泄漏,考慮下面的例子:
class A{
public A(){
B.getInstance().setA(this);
}
....
}
//B類采用單例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter...
}
顯然B采用singleton模式于樟,它持有一個(gè)A對(duì)象的引用公条,而這個(gè)A類的對(duì)象將不能被回收。想象下如果A是個(gè)比較復(fù)雜的對(duì)象或者集合類型會(huì)發(fā)生什么情況
Android中常見的內(nèi)存泄漏匯總
集合類泄漏
集合類如果僅僅有添加元素的方法迂曲,而沒有相應(yīng)的刪除機(jī)制靶橱,導(dǎo)致內(nèi)存被占用。如果這個(gè)集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它)关霸,那么沒有相應(yīng)的刪除機(jī)制传黄,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。比如上面的典型例子就是其中一種情況队寇,當(dāng)然實(shí)際上我們?cè)陧?xiàng)目中肯定不會(huì)寫這么 2B 的代碼膘掰,但稍不注意還是很容易出現(xiàn)這種情況,比如我們都喜歡通過 HashMap 做一些緩存之類的事佳遣,這種情況就要多留一些心眼识埋。
單例造成的內(nèi)存泄漏
由于單例的靜態(tài)特性使得其生命周期跟應(yīng)用的生命周期一樣長(zhǎng),所以如果使用不恰當(dāng)?shù)脑捔憬ィ苋菀自斐蓛?nèi)存泄漏窒舟。比如下面一個(gè)典型的例子,
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}
這是一個(gè)普通的單例模式诵盼,當(dāng)創(chuàng)建這個(gè)單例的時(shí)候辜纲,由于需要傳入一個(gè)Context,所以這個(gè)Context的生命周期的長(zhǎng)短至關(guān)重要:
1拦耐、如果此時(shí)傳入的是 Application 的 Context耕腾,因?yàn)?Application 的生命周期就是整個(gè)應(yīng)用的生命周期,所以這將沒有任何問題杀糯。
2扫俺、如果此時(shí)傳入的是 Activity 的 Context,當(dāng)這個(gè) Context 所對(duì)應(yīng)的 Activity 退出時(shí)固翰,由于該 Context 的引用被單例對(duì)象所持有狼纬,其生命周期等于整個(gè)應(yīng)用程序的生命周期,所以當(dāng)前 Activity 退出時(shí)它的內(nèi)存并不會(huì)被回收骂际,這就造成泄漏了疗琉。
正確的方式應(yīng)該改為下面這種方式:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context.getApplicationContext();// 使用Application 的context
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}
或者這樣寫,連 Context 都不用傳進(jìn)來了:
在你的 Application 中添加一個(gè)靜態(tài)方法歉铝,getContext() 返回 Application 的 context盈简,
...
context = getApplicationContext();
...
/**
* 獲取全局的context
* @return 返回全局context對(duì)象
*/
public static Context getContext(){
return context;
}
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager() {
this.context = MyApplication.getContext();// 使用Application 的context
}
public static AppManager getInstance() {
if (instance == null) {
instance = new AppManager();
}
return instance;
}
}
匿名內(nèi)部類/非靜態(tài)內(nèi)部類和異步線程
非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實(shí)例造成的內(nèi)存泄漏
有的時(shí)候我們可能會(huì)在啟動(dòng)頻繁的Activity中,為了避免重復(fù)創(chuàng)建相同的數(shù)據(jù)資源太示,可能會(huì)出現(xiàn)這種寫法:
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mManager == null){
mManager = new TestResource();
}
//...
}
class TestResource {
//...
}
}
這樣就在Activity內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例柠贤,每次啟動(dòng)Activity時(shí)都會(huì)使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復(fù)創(chuàng)建类缤,不過這種寫法卻會(huì)造成內(nèi)存泄漏臼勉,因?yàn)榉庆o態(tài)內(nèi)部類默認(rèn)會(huì)持有外部類的引用,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個(gè)靜態(tài)的實(shí)例餐弱,該實(shí)例的生命周期和應(yīng)用的一樣長(zhǎng)宴霸,這就導(dǎo)致了該靜態(tài)實(shí)例一直會(huì)持有該Activity的引用囱晴,導(dǎo)致Activity的內(nèi)存資源不能正常回收瓢谢。正確的做法為:
將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個(gè)單例畸写,如果需要使用Context,請(qǐng)按照上面推薦的使用Application 的 Context恩闻。當(dāng)然,Application 的 context 不是萬能的剧董,所以也不能隨便亂用幢尚,對(duì)于有些地方則必須使用 Activity 的 Context,對(duì)于Application翅楼,Service尉剩,Activity三者的Context的應(yīng)用場(chǎng)景如下:
其中: NO1表示 Application 和 Service 可以啟動(dòng)一個(gè) Activity,不過需要?jiǎng)?chuàng)建一個(gè)新的 task 任務(wù)隊(duì)列毅臊。而對(duì)于 Dialog 而言理茎,只有在 Activity 中才能創(chuàng)建
匿名內(nèi)部類
android開發(fā)經(jīng)常會(huì)繼承實(shí)現(xiàn)Activity/Fragment/View,此時(shí)如果你使用了匿名類管嬉,并被異步線程持有了皂林,那要小心了,如果沒有任何措施這樣一定會(huì)導(dǎo)致泄露
public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
...
}
ref1和ref2的區(qū)別是蚯撩,ref2使用了匿名內(nèi)部類础倍。我們來看看運(yùn)行時(shí)這兩個(gè)引用的內(nèi)存:
可以看到,ref1沒什么特別的胎挎。
但ref2這個(gè)匿名類的實(shí)現(xiàn)對(duì)象里面多了一個(gè)引用:
this$0這個(gè)引用指向MainActivity.this沟启,也就是說當(dāng)前的MainActivity實(shí)例會(huì)被ref2持有,如果將這個(gè)引用再傳入一個(gè)異步線程犹菇,此線程和此Acitivity生命周期不一致的時(shí)候德迹,就造成了Activity的泄露。
Handler 造成的內(nèi)存泄漏
Handler 的使用造成的內(nèi)存泄漏問題應(yīng)該說是最為常見了揭芍,很多時(shí)候我們?yōu)榱吮苊?ANR 而不在主線程進(jìn)行耗時(shí)操作胳搞,在處理網(wǎng)絡(luò)任務(wù)或者封裝一些請(qǐng)求回調(diào)等api都借助Handler來處理,但 Handler 不是萬能的称杨,對(duì)于 Handler 的使用代碼編寫一不規(guī)范即有可能造成內(nèi)存泄漏流酬。另外,我們知道 Handler列另、Message 和 MessageQueue 都是相互關(guān)聯(lián)在一起的芽腾,萬一 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對(duì)象將被線程 MessageQueue 一直持有页衙。
由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的摊滔。因此這種實(shí)現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致阴绢,故很容易導(dǎo)致無法正確釋放。
舉個(gè)例子:
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
在該 SampleActivity 中聲明了一個(gè)延遲10分鐘執(zhí)行的消息 Message艰躺,mLeakyHandler 將其 push 進(jìn)了消息隊(duì)列 MessageQueue 里呻袭。當(dāng)該 Activity 被 finish() 掉時(shí),延遲執(zhí)行任務(wù)的 Message 還會(huì)繼續(xù)存在于主線程中腺兴,它持有該 Activity 的 Handler 引用左电,所以此時(shí) finish() 掉的 Activity 就不會(huì)被回收了從而造成內(nèi)存泄漏(因 Handler 為非靜態(tài)內(nèi)部類,它會(huì)持有外部類的引用页响,在這里就是指 SampleActivity)篓足。
修復(fù)方法:在 Activity 中避免使用非靜態(tài)內(nèi)部類,比如上面我們將 Handler 聲明為靜態(tài)的闰蚕,則其存活期跟 Activity 的生命周期就無關(guān)了栈拖。同時(shí)通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進(jìn)去没陡,見下面代碼:
public class SampleActivity extends Activity {
/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
private final MyHandler mHandler = new MyHandler(this);
/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
綜述涩哟,即推薦使用靜態(tài)內(nèi)部類 + WeakReference 這種方式。每次使用前注意判空盼玄。
前面提到了 WeakReference贴彼,所以這里就簡(jiǎn)單的說一下 Java 對(duì)象的幾種引用類型。
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ì)象。
private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();
再來定義一個(gè)方法羹奉,保存Bitmap的軟引用到HashMap秒旋。

使用軟引用以后,在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ì)象不被使用的可能性更大些革骨,就可以用弱引用。
ok析恋,繼續(xù)回到主題良哲。前面所說的,創(chuàng)建一個(gè)靜態(tài)Handler內(nèi)部類助隧,然后對(duì) Handler 持有的對(duì)象使用弱引用筑凫,這樣在回收時(shí)也可以回收 Handler 持有的對(duì)象,但是這樣做雖然避免了 Activity 泄漏并村,不過 Looper 線程的消息隊(duì)列中還是可能會(huì)有待處理的消息巍实,所以我們?cè)?Activity 的 Destroy 時(shí)或者 Stop 時(shí)應(yīng)該移除消息隊(duì)列 MessageQueue 中的消息。
下面幾個(gè)方法都可以移除 Message:
public final void removeCallbacks(Runnable r);
public final void removeCallbacks(Runnable r, Object token);
public final void removeCallbacksAndMessages(Object token);
public final void removeMessages(int what);
public final void removeMessages(int what, Object object);
盡量避免使用 static 成員變量
如果成員變量被聲明為 static哩牍,那我們都知道其生命周期將與整個(gè)app進(jìn)程生命周期一樣棚潦。
這會(huì)導(dǎo)致一系列問題,如果你的app進(jìn)程設(shè)計(jì)上是長(zhǎng)駐內(nèi)存的膝昆,那即使app切到后臺(tái)瓦盛,這部分內(nèi)存也不會(huì)被釋放洗显。按照現(xiàn)在手機(jī)app內(nèi)存管理機(jī)制,占內(nèi)存較大的后臺(tái)進(jìn)程將優(yōu)先回收原环,yi'wei如果此app做過進(jìn)程互保蹦铀簦活,那會(huì)造成app在后臺(tái)頻繁重啟嘱吗。當(dāng)手機(jī)安裝了你參與開發(fā)的app以后一夜時(shí)間手機(jī)被消耗空了電量玄组、流量,你的app不得不被用戶卸載或者靜默谒麦。
這里修復(fù)的方法是:
不要在類初始時(shí)初始化靜態(tài)成員俄讹。可以考慮lazy初始化绕德。
架構(gòu)設(shè)計(jì)上要思考是否真的有必要這樣做患膛,盡量避免。如果架構(gòu)需要這么設(shè)計(jì)耻蛇,那么此對(duì)象的生命周期你有責(zé)任管理起來踪蹬。
避免 override finalize()
1、finalize 方法被執(zhí)行的時(shí)間不確定臣咖,不能依賴與它來釋放緊缺的資源跃捣。時(shí)間不確定的原因是:
虛擬機(jī)調(diào)用GC的時(shí)間不確定
Finalize daemon線程被調(diào)度到的時(shí)間不確定
2、finalize 方法只會(huì)被執(zhí)行一次夺蛇,即使對(duì)象被復(fù)活疚漆,如果已經(jīng)執(zhí)行過了 finalize 方法,再次被 GC 時(shí)也不會(huì)再執(zhí)行了刁赦,原因是:
含有 finalize 方法的 object 是在 new 的時(shí)候由虛擬機(jī)生成了一個(gè) finalize reference 在來引用到該Object的娶聘,而在 finalize 方法執(zhí)行的時(shí)候,該 object 所對(duì)應(yīng)的 finalize Reference 會(huì)被釋放掉甚脉,即使在這個(gè)時(shí)候把該 object 復(fù)活(即用強(qiáng)引用引用住該 object )丸升,再第二次被 GC 的時(shí)候由于沒有了 finalize reference 與之對(duì)應(yīng),所以 finalize 方法不會(huì)再執(zhí)行宦焦。
3发钝、含有Finalize方法的object需要至少經(jīng)過兩輪GC才有可能被釋放。
資源未關(guān)閉造成的內(nèi)存泄漏
對(duì)于使用了BraodcastReceiver波闹,ContentObserver酝豪,F(xiàn)ile,游標(biāo) Cursor精堕,Stream孵淘,Bitmap等資源的使用,應(yīng)該在Activity銷毀時(shí)及時(shí)關(guān)閉或者注銷歹篓,否則這些資源將不會(huì)被回收瘫证,造成內(nèi)存泄漏揉阎。
一些不良代碼造成的內(nèi)存壓力
有些代碼并不造成內(nèi)存泄露,但是它們背捌,或是對(duì)沒使用的內(nèi)存沒進(jìn)行有效及時(shí)的釋放毙籽,或是沒有有效的利用已有的對(duì)象而是頻繁的申請(qǐng)新內(nèi)存。
比如:
Bitmap 沒調(diào)用 recycle()方法毡庆,對(duì)于 Bitmap 對(duì)象在不使用時(shí),我們應(yīng)該先調(diào)用 recycle() 釋放內(nèi)存坑赡,然后才它設(shè)置為 null. 因?yàn)榧虞d Bitmap 對(duì)象的內(nèi)存空間,一部分是 java 的么抗,一部分 C 的(因?yàn)?Bitmap 分配的底層是通過 JNI 調(diào)用的 )毅否。 而這個(gè) recyle() 就是針對(duì) C 部分的內(nèi)存釋放。
構(gòu)造 Adapter 時(shí)蝇刀,沒有使用緩存的 convertView ,每次都在創(chuàng)建新的 converView螟加。這里推薦使用 ViewHolder。
總結(jié)
對(duì) Activity 等組件的引用應(yīng)該控制在 Activity 的生命周期之內(nèi)吞琐; 如果不能就考慮使用 getApplicationContext 或者 getApplication捆探,以避免 Activity 被外部長(zhǎng)生命周期的對(duì)象引用而泄露。
盡量不要在靜態(tài)變量或者靜態(tài)內(nèi)部類中使用非靜態(tài)外部成員變量(包括context )顽分,即使要使用徐许,也要考慮適時(shí)把外部成員變量置空施蜜;也可以在內(nèi)部類中使用弱引用來引用外部類的變量卒蘸。
對(duì)于生命周期比Activity長(zhǎng)的內(nèi)部類對(duì)象,并且內(nèi)部類中使用了外部類的成員變量翻默,可以這樣做避免內(nèi)存泄漏:
將內(nèi)部類改為靜態(tài)內(nèi)部類
靜態(tài)內(nèi)部類中使用弱引用來引用外部類的成員變量
Handler 的持有的引用對(duì)象最好使用弱引用缸沃,資源釋放時(shí)也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的時(shí)候修械,取消掉該 Handler 對(duì)象的 Message和 Runnable.
在 Java 的實(shí)現(xiàn)過程中趾牧,也要考慮其對(duì)象釋放,最好的方法是在不使用某對(duì)象時(shí)肯污,顯式地將此對(duì)象賦值為 null翘单,比如使用完Bitmap 后先調(diào)用 recycle(),再賦為null,清空對(duì)圖片等資源有直接引用或者間接引用的數(shù)組(使用 array.clear() ; array = null)等蹦渣,最好遵循誰創(chuàng)建誰釋放的原則哄芜。
正確關(guān)閉資源,對(duì)于使用了BraodcastReceiver柬唯,ContentObserver认臊,F(xiàn)ile,游標(biāo) Cursor锄奢,Stream失晴,Bitmap等資源的使用剧腻,應(yīng)該在Activity銷毀時(shí)及時(shí)關(guān)閉或者注銷。
保持對(duì)對(duì)象生命周期的敏感涂屁,特別注意單例书在、靜態(tài)對(duì)象、全局性集合等的生命周期拆又。