Android 內(nèi)存管理的目的
內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題轮傍。簡單粗俗的講汽馋,就是該被釋放的對象沒有釋放纺讲,一直被某個或某些實例所持有卻不再被使用導(dǎo)致 GC 不能回收未檩。
Java 內(nèi)存分配策略
Java 程序運(yùn)行時的內(nèi)存分配策略有三種,分別是靜態(tài)分配,棧式分配,和堆式分配,對應(yīng)的粥航,三種存儲策略使用的內(nèi)存空間主要分別是靜態(tài)存儲區(qū)(也稱方法區(qū))琅捏、棧區(qū)和堆區(qū)。
靜態(tài)存儲區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù)递雀、全局 static 數(shù)據(jù)和常量柄延。這塊內(nèi)存在程序編譯時就已經(jīng)分配好,并且在程序整個運(yùn)行期間都存在缀程。
棧區(qū) :當(dāng)方法被執(zhí)行時搜吧,方法體內(nèi)的局部變量(其中包括基礎(chǔ)數(shù)據(jù)類型、對象的引用)都在棧上創(chuàng)建杨凑,并在方法執(zhí)行結(jié)束時這些局部變量所持有的內(nèi)存將會自動被釋放滤奈。因為棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高撩满,但是分配的內(nèi)存容量有限蜒程。
堆區(qū) : 又稱動態(tài)內(nèi)存分配,通常就是指在程序運(yùn)行時直接 new 出來的內(nèi)存伺帘,也就是對象的實例昭躺。這部分內(nèi)存在不使用時將會由 Java 垃圾回收器來負(fù)責(zé)回收。
棧與堆的區(qū)別:
在方法體內(nèi)定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內(nèi)存中分配的伪嫁。當(dāng)在一段方法塊中定義一個變量時领炫,Java 就會在棧中為該變量分配內(nèi)存空間,當(dāng)超過該變量的作用域后张咳,該變量也就無效了帝洪,分配給它的內(nèi)存空間也將被釋放掉针史,該內(nèi)存空間可以被重新使用。
堆內(nèi)存用來存放所有由 new 創(chuàng)建的對象(包括該對象其中的所有成員變量)和數(shù)組碟狞。在堆中分配的內(nèi)存,將由 Java 垃圾回收器來自動管理婚陪。在堆中產(chǎn)生了一個數(shù)組或者對象后族沃,還可以在棧中定義一個特殊的變量,這個變量的取值等于數(shù)組或者對象在堆內(nèi)存中的首地址泌参,這個特殊的變量就是我們上面說的引用變量脆淹。我們可以通過這個引用變量來訪問堆中的對象或者數(shù)組。
舉個例子:
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 指向的對象是存在于堆上的盖溺。
mSample3 指向的對象實體存放在堆上,包括這個對象的所有成員變量 s1 和 mSample1铣缠,而它自己存在于棧中烘嘱。
結(jié)論:
局部變量的基本數(shù)據(jù)類型和引用存儲于棧中,引用的對象實體存儲于堆中蝗蛙∮ィ—— 因為它們屬于方法中的變量,生命周期隨方法而結(jié)束捡硅。
成員變量全部存儲與堆中(包括基本數(shù)據(jù)類型哮内,引用和引用的對象實體)—— 因為它們屬于類,類對象終究是要被new出來使用的壮韭。
了解了 Java 的內(nèi)存分配之后北发,我們再來看看 Java 是怎么管理內(nèi)存的。
Java是如何管理內(nèi)存
Java的內(nèi)存管理就是對象的分配和釋放問題喷屋。在 Java 中琳拨,程序員需要通過關(guān)鍵字 new 為每個對象申請內(nèi)存空間 (基本類型除外),所有的對象都在堆 (Heap)中分配空間屯曹。另外从绘,對象的釋放是由 GC 決定和執(zhí)行的。在 Java 中是牢,內(nèi)存的分配是由程序完成的僵井,而內(nèi)存的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程序員的工作驳棱。但同時批什,它也加重了JVM的工作。這也是 Java 程序運(yùn)行速度較慢的原因之一社搅。因為驻债,GC 為了能夠正確釋放對象乳规,GC 必須監(jiān)控每一個對象的運(yùn)行狀態(tài),包括對象的申請合呐、引用暮的、被引用、賦值等淌实,GC 都需要進(jìn)行監(jiān)控冻辩。
監(jiān)視對象狀態(tài)是為了更加準(zhǔn)確地、及時地釋放對象拆祈,而釋放對象的根本原則就是該對象不再被引用恨闪。
為了更好理解 GC 的工作原理,我們可以將對象考慮為有向圖的頂點放坏,將引用關(guān)系考慮為圖的有向邊咙咽,有向邊從引用者指向被引對象。另外淤年,每個線程對象可以作為一個圖的起始頂點钧敞,例如大多程序從 main 進(jìn)程開始執(zhí)行,那么該圖就是以 main 進(jìn)程頂點開始的一棵根樹麸粮。在這個有向圖中犁享,根頂點可達(dá)的對象都是有效對象,GC將不回收這些對象豹休。如果某個對象 (連通子圖)與這個根頂點不可達(dá)(注意炊昆,該圖為有向圖),那么我們認(rèn)為這個(這些)對象不再被引用威根,可以被 GC 回收凤巨。
以下,我們舉一個例子說明如何用有向圖表示內(nèi)存管理洛搀。對于程序的每一個時刻敢茁,我們都有一個有向圖表示JVM的內(nèi)存分配情況。以下右圖留美,就是左邊程序運(yùn)行到第6行的示意圖彰檬。
Java使用有向圖的方式進(jìn)行內(nèi)存管理,可以消除引用循環(huán)的問題谎砾,例如有三個對象逢倍,相互引用塘幅,只要它們和根進(jìn)程不可達(dá)的眼耀,那么GC也是可以回收它們的。這種方式的優(yōu)點是管理內(nèi)存的精度很高食拜,但是效率較低。另外一種常用的內(nèi)存管理技術(shù)是使用計數(shù)器亮蒋,例如COM模型采用計數(shù)器方式管理構(gòu)件扣典,它與有向圖相比,精度行低(很難處理循環(huán)引用的問題)慎玖,但執(zhí)行效率很高贮尖。
什么是Java中的內(nèi)存泄露
在Java中,內(nèi)存泄漏就是存在一些被分配的對象趁怔,這些對象有下面兩個特點湿硝,首先,這些對象是可達(dá)的痕钢,即在有向圖中,存在通路可以與其相連序六;其次任连,這些對象是無用的,即程序以后不會再使用這些對象例诀。如果對象滿足這兩個條件随抠,這些對象就可以判定為Java中的內(nèi)存泄漏,這些對象不會被GC所回收繁涂,然而它卻占用內(nèi)存拱她。
在C++中,內(nèi)存泄漏的范圍更大一些扔罪。有些對象被分配了內(nèi)存空間秉沼,然后卻不可達(dá),由于C++中沒有GC矿酵,這些內(nèi)存將永遠(yuǎn)收不回來唬复。在Java中,這些不可達(dá)的對象都由GC負(fù)責(zé)回收全肮,因此程序員不需要考慮這部分的內(nèi)存泄露敞咧。
通過分析,我們得知辜腺,對于C++休建,程序員需要自己管理邊和頂點,而對于Java程序員只需要管理邊就可以了(不需要管理頂點的釋放)评疗。通過這種方式测砂,Java提高了編程的效率。
因此百匆,通過以上分析邑彪,我們知道在Java中也有內(nèi)存泄漏,但范圍比C++要小一些胧华。因為Java從語言上保證寄症,任何對象都是可達(dá)的宙彪,所有的不可達(dá)對象都由GC管理。
對于程序員來說有巧,GC基本是透明的释漆,不可見的。雖然篮迎,我們只有幾個函數(shù)可以訪問GC男图,例如運(yùn)行GC的函數(shù)System.gc(),但是根據(jù)Java語言規(guī)范定義甜橱, 該函數(shù)不保證JVM的垃圾收集器一定會執(zhí)行逊笆。因為,不同的JVM實現(xiàn)者可能使用不同的算法管理GC岂傲。通常难裆,GC的線程的優(yōu)先級別較低。JVM調(diào)用GC的策略也有很多種镊掖,有的是內(nèi)存使用到達(dá)一定程度時乃戈,GC才開始工作,也有定時執(zhí)行的亩进,有的是平緩執(zhí)行GC症虑,有的是中斷式執(zhí)行GC。但通常來說归薛,我們不需要關(guān)心這些谍憔。除非在一些特定的場合,GC的執(zhí)行影響應(yīng)用程序的性能主籍,例如對于基于Web的實時系統(tǒng)韵卤,如網(wǎng)絡(luò)游戲等,用戶不希望GC突然中斷應(yīng)用程序執(zhí)行而進(jìn)行垃圾回收崇猫,那么我們需要調(diào)整GC的參數(shù)沈条,讓GC能夠通過平緩的方式釋放內(nèi)存,例如將垃圾回收分解為一系列的小步驟執(zhí)行诅炉,Sun提供的HotSpot JVM就支持這一特性蜡歹。
同樣給出一個 Java 內(nèi)存泄漏的典型例子,
Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
在這個例子中涕烧,我們循環(huán)申請Object對象月而,并將所申請的對象放入一個 Vector 中,如果我們僅僅釋放引用本身议纯,那么 Vector 仍然引用該對象父款,所以這個對象對 GC 來說是不可回收的。因此,如果對象加入到Vector 后憨攒,還必須從 Vector 中刪除世杀,最簡單的方法就是將 Vector 對象設(shè)置為 null。
詳細(xì)Java中的內(nèi)存泄漏
1.Java內(nèi)存回收機(jī)制
不論哪種語言的內(nèi)存分配方式肝集,都需要返回所分配內(nèi)存的真實地址瞻坝,也就是返回一個指針到內(nèi)存塊的首地址。Java中對象是采用new或者反射的方法創(chuàng)建的杏瞻,這些對象的創(chuàng)建都是在堆(Heap)中分配的所刀,所有對象的回收都是由Java虛擬機(jī)通過垃圾回收機(jī)制完成的。GC為了能夠正確釋放對象捞挥,會監(jiān)控每個對象的運(yùn)行狀況浮创,對他們的申請、引用砌函、被引用斩披、賦值等狀況進(jìn)行監(jiān)控,Java會使用有向圖的方法進(jìn)行管理內(nèi)存胸嘴,實時監(jiān)控對象是否可以達(dá)到雏掠,如果不可到達(dá)斩祭,則就將其回收劣像,這樣也可以消除引用循環(huán)的問題。在Java語言中摧玫,判斷一個內(nèi)存空間是否符合垃圾收集標(biāo)準(zhǔn)有兩個:一個是給對象賦予了空值null耳奕,以下再沒有調(diào)用過,另一個是給對象賦予了新值诬像,這樣重新分配了內(nèi)存空間屋群。
2.Java內(nèi)存泄漏引起的原因
內(nèi)存泄漏是指無用對象(不再使用的對象)持續(xù)占有內(nèi)存或無用對象的內(nèi)存得不到及時釋放,從而造成內(nèi)存空間的浪費稱為內(nèi)存泄漏坏挠。內(nèi)存泄露有時不嚴(yán)重且不易察覺芍躏,這樣開發(fā)者就不知道存在內(nèi)存泄露,但有時也會很嚴(yán)重降狠,會提示你Out of memory对竣。j
Java內(nèi)存泄漏的根本原因是什么呢?長生命周期的對象持有短生命周期對象的引用就很可能發(fā)生內(nèi)存泄漏榜配,盡管短生命周期對象已經(jīng)不再需要否纬,但是因為長生命周期持有它的引用而導(dǎo)致不能被回收,這就是Java中內(nèi)存泄漏的發(fā)生場景蛋褥。具體主要有如下幾大類:
1临燃、靜態(tài)集合類引起內(nèi)存泄漏:
像HashMap、Vector等的使用最容易出現(xiàn)內(nèi)存泄露,這些靜態(tài)變量的生命周期和應(yīng)用程序一致膜廊,他們所引用的所有的對象Object也不能被釋放乏沸,因為他們也將一直被Vector等引用著。
例如
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
在這個例子中溃论,循環(huán)申請Object 對象屎蜓,并將所申請的對象放入一個Vector 中,如果僅僅釋放引用本身(o=null)钥勋,那么Vector 仍然引用該對象炬转,所以這個對象對GC 來說是不可回收的。因此算灸,如果對象加入到Vector 后扼劈,還必須從Vector 中刪除,最簡單的方法就是將Vector對象設(shè)置為null菲驴。
2荐吵、當(dāng)集合里面的對象屬性被修改后,再調(diào)用remove()方法時不起作用赊瞬。
例如:
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()+" 個元素!"); //結(jié)果:總共有:3 個元素!
p3.setAge(2); //修改p3的年齡,此時p3元素對應(yīng)的hashcode值發(fā)生改變
set.remove(p3); //此時remove不掉先煎,造成內(nèi)存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println("總共有:"+set.size()+" 個元素!"); //結(jié)果:總共有:4 個元素!
for (Person person : set)
{
System.out.println(person);
}
}
3巧涧、監(jiān)聽器
在java 編程中薯蝎,我們都需要和監(jiān)聽器打交道,通常一個應(yīng)用當(dāng)中會用到很多監(jiān)聽器谤绳,我們會調(diào)用一個控件的諸如addXXXListener()等方法來增加監(jiān)聽器占锯,但往往在釋放對象的時候卻沒有記住去刪除這些監(jiān)聽器,從而增加了內(nèi)存泄漏的機(jī)會缩筛。
4消略、各種連接
比如數(shù)據(jù)庫連接(dataSourse.getConnection()),網(wǎng)絡(luò)連接(socket)和io連接瞎抛,除非其顯式的調(diào)用了其close()方法將其連接關(guān)閉艺演,否則是不會自動被GC 回收的。對于Resultset 和Statement 對象可以不進(jìn)行顯式回收桐臊,但Connection 一定要顯式回收胎撤,因為Connection 在任何時候都無法自動回收,而Connection一旦回收豪硅,Resultset 和Statement 對象就會立即為NULL哩照。但是如果使用連接池,情況就不一樣了懒浮,除了要顯式地關(guān)閉連接飘弧,還必須顯式地關(guān)閉Resultset Statement 對象(關(guān)閉其中一個识藤,另外一個也會關(guān)閉),否則就會造成大量的Statement 對象無法釋放次伶,從而引起內(nèi)存泄漏痴昧。這種情況下一般都會在try里面去的連接,在finally里面釋放連接冠王。
5赶撰、內(nèi)部類和外部模塊的引用
內(nèi)部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導(dǎo)致一系列的后繼類對象沒有釋放柱彻。此外程序員還要小心外部模塊不經(jīng)意的引用豪娜,例如程序員A 負(fù)責(zé)A 模塊,調(diào)用了B 模塊的一個方法如:
public void registerMsg(Object b);
這種調(diào)用就要非常小心了哟楷,傳入了一個對象瘤载,很可能模塊B就保持了對該對象的引用,這時候就需要注意模塊B 是否提供相應(yīng)的操作去除引用卖擅。
6鸣奔、單例模式
不正確使用單例模式是引起內(nèi)存泄漏的一個常見問題,單例對象在初始化后將在JVM的整個生命周期中存在(以靜態(tài)變量的方式)惩阶,如果單例對象持有外部的引用挎狸,那么這個對象將不能被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模式,它持有一個A對象的引用脐嫂,而這個A類的對象將不能被回收统刮。想象下如果A是個比較復(fù)雜的對象或者集合類型會發(fā)生什么情況
Android中常見的內(nèi)存泄漏匯總
集合類泄漏
集合類如果僅僅有添加元素的方法紊遵,而沒有相應(yīng)的刪除機(jī)制账千,導(dǎo)致內(nèi)存被占用。如果這個集合類是全局性的變量 (比如類中的靜態(tài)屬性暗膜,全局性的 map 等即有靜態(tài)引用或 final 一直指向它)匀奏,那么沒有相應(yīng)的刪除機(jī)制,很可能導(dǎo)致集合所占用的內(nèi)存只增不減学搜。比如上面的典型例子就是其中一種情況娃善,當(dāng)然實際上我們在項目中肯定不會寫這么 2B 的代碼,但稍不注意還是很容易出現(xiàn)這種情況瑞佩,比如我們都喜歡通過 HashMap 做一些緩存之類的事聚磺,這種情況就要多留一些心眼。
單例造成的內(nèi)存泄漏
由于單例的靜態(tài)特性使得其生命周期跟應(yīng)用的生命周期一樣長炬丸,所以如果使用不恰當(dāng)?shù)脑捥鼻蓿苋菀自斐蓛?nèi)存泄漏蜒蕾。比如下面一個典型的例子,
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;
}
}
這是一個普通的單例模式焕阿,當(dāng)創(chuàng)建這個單例的時候咪啡,由于需要傳入一個Context,所以這個Context的生命周期的長短至關(guān)重要:
1暮屡、如果此時傳入的是 Application 的 Context撤摸,因為 Application 的生命周期就是整個應(yīng)用的生命周期,所以這將沒有任何問題褒纲。
2准夷、如果此時傳入的是 Activity 的 Context,當(dāng)這個 Context 所對應(yīng)的 Activity 退出時莺掠,由于該 Context 的引用被單例對象所持有冕象,其生命周期等于整個應(yīng)用程序的生命周期,所以當(dāng)前 Activity 退出時它的內(nèi)存并不會被回收汁蝶,這就造成泄漏了渐扮。
正確的方式應(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 中添加一個靜態(tài)方法掖棉,getContext() 返回 Application 的 context墓律,
...
context = getApplicationContext();
...
/**
* 獲取全局的context
* @return 返回全局context對象
*/
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)實例造成的內(nèi)存泄漏
有的時候我們可能會在啟動頻繁的Activity中,為了避免重復(fù)創(chuàng)建相同的數(shù)據(jù)資源幔亥,可能會出現(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)建了一個非靜態(tài)內(nèi)部類的單例耻讽,每次啟動Activity時都會使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復(fù)創(chuàng)建帕棉,不過這種寫法卻會造成內(nèi)存泄漏针肥,因為非靜態(tài)內(nèi)部類默認(rèn)會持有外部類的引用,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個靜態(tài)的實例香伴,該實例的生命周期和應(yīng)用的一樣長慰枕,這就導(dǎo)致了該靜態(tài)實例一直會持有該Activity的引用,導(dǎo)致Activity的內(nèi)存資源不能正臣锤伲回收具帮。正確的做法為:
將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個單例,如果需要使用Context低斋,請按照上面推薦的使用Application 的 Context蜂厅。當(dāng)然,Application 的 context 不是萬能的膊畴,所以也不能隨便亂用掘猿,對于有些地方則必須使用 Activity 的 Context,對于Application唇跨,Service稠通,Activity三者的Context的應(yīng)用場景如下:
其中: NO1表示 Application 和 Service 可以啟動一個 Activity礁遵,不過需要創(chuàng)建一個新的 task 任務(wù)隊列。而對于 Dialog 而言采记,只有在 Activity 中才能創(chuàng)建
匿名內(nèi)部類
android開發(fā)經(jīng)常會繼承實現(xiàn)Activity/Fragment/View佣耐,此時如果你使用了匿名類,并被異步線程持有了唧龄,那要小心了兼砖,如果沒有任何措施這樣一定會導(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)行時這兩個引用的內(nèi)存:
可以看到讽挟,ref1沒什么特別的。
但ref2這個匿名類的實現(xiàn)對象里面多了一個引用:
this$0這個引用指向MainActivity.this丸冕,也就是說當(dāng)前的MainActivity實例會被ref2持有耽梅,如果將這個引用再傳入一個異步線程,此線程和此Acitivity生命周期不一致的時候胖烛,就造成了Activity的泄露眼姐。
Handler 造成的內(nèi)存泄漏
Handler 的使用造成的內(nèi)存泄漏問題應(yīng)該說是最為常見了,很多時候我們?yōu)榱吮苊?ANR 而不在主線程進(jìn)行耗時操作佩番,在處理網(wǎng)絡(luò)任務(wù)或者封裝一些請求回調(diào)等api都借助Handler來處理众旗,但 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 是不一致的猎莲。因此這種實現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致绍弟,故很容易導(dǎo)致無法正確釋放。
舉個例子:
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 中聲明了一個延遲10分鐘執(zhí)行的消息 Message益眉,mLeakyHandler 將其 push 進(jìn)了消息隊列 MessageQueue 里晌柬。當(dāng)該 Activity 被 finish() 掉時姥份,延遲執(zhí)行任務(wù)的 Message 還會繼續(xù)存在于主線程中郭脂,它持有該 Activity 的 Handler 引用,所以此時 finish() 掉的 Activity 就不會被回收了從而造成內(nèi)存泄漏(因 Handler 為非靜態(tài)內(nèi)部類澈歉,它會持有外部類的引用展鸡,在這里就是指 SampleActivity)。
修復(fù)方法:在 Activity 中避免使用非靜態(tài)內(nèi)部類埃难,比如上面我們將 Handler 聲明為靜態(tài)的莹弊,則其存活期跟 Activity 的生命周期就無關(guān)了涤久。同時通過弱引用的方式引入 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蔗彤,所以這里就簡單的說一下 Java 對象的幾種引用類型。
Java對引用的分類有 Strong reference, SoftReference, WeakReference, PhatomReference 四種疯兼。
[圖片上傳失敗...(image-d1a6cd-1526646725116)]
在Android應(yīng)用的開發(fā)中然遏,為了防止內(nèi)存溢出,在處理一些占用內(nèi)存大而且聲明周期較長的對象時候吧彪,可以盡量應(yīng)用軟引用和弱引用技術(shù)待侵。
軟/弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對象被垃圾回收器回收姨裸,Java虛擬機(jī)就會把這個軟引用加入到與之關(guān)聯(lián)的引用隊列中秧倾。利用這個隊列可以得知被回收的軟/弱引用的對象列表,從而為緩沖器清除已失效的軟/弱引用傀缩。
假設(shè)我們的應(yīng)用會用到大量的默認(rèn)圖片中狂,比如應(yīng)用中有默認(rèn)的頭像,默認(rèn)游戲圖標(biāo)等等扑毡,這些圖片很多地方會用到胃榕。如果每次都去讀取圖片,由于讀取文件需要硬件操作瞄摊,速度較慢勋又,會導(dǎo)致性能較低。所以我們考慮將圖片緩存起來换帜,需要的時候直接從內(nèi)存中讀取楔壤。但是,由于圖片占用內(nèi)存空間比較大惯驼,緩存很多圖片需要很多的內(nèi)存蹲嚣,就可能比較容易發(fā)生OutOfMemory異常。這時祟牲,我們可以考慮使用軟/弱引用技術(shù)來避免這個問題發(fā)生隙畜。以下就是高速緩沖器的雛形:
首先定義一個HashMap,保存軟引用對象说贝。
private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();
再來定義一個方法议惰,保存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)建一個靜態(tài)Handler內(nèi)部類,然后對 Handler 持有的對象使用弱引用梆砸,這樣在回收時也可以回收 Handler 持有的對象转质,但是這樣做雖然避免了 Activity 泄漏,不過 Looper 線程的消息隊列中還是可能會有待處理的消息帖世,所以我們在 Activity 的 Destroy 時或者 Stop 時應(yīng)該移除消息隊列 MessageQueue 中的消息休蟹。
下面幾個方法都可以移除 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,那我們都知道其生命周期將與整個app進(jìn)程生命周期一樣日矫。
這會導(dǎo)致一系列問題赂弓,如果你的app進(jìn)程設(shè)計上是長駐內(nèi)存的,那即使app切到后臺哪轿,這部分內(nèi)存也不會被釋放盈魁。按照現(xiàn)在手機(jī)app內(nèi)存管理機(jī)制,占內(nèi)存較大的后臺進(jìn)程將優(yōu)先回收窃诉,yi'wei如果此app做過進(jìn)程互保毖畎遥活,那會造成app在后臺頻繁重啟飘痛。當(dāng)手機(jī)安裝了你參與開發(fā)的app以后一夜時間手機(jī)被消耗空了電量珊膜、流量票编,你的app不得不被用戶卸載或者靜默。
這里修復(fù)的方法是:
不要在類初始時初始化靜態(tài)成員凡桥∶兀可以考慮lazy初始化掖疮。
架構(gòu)設(shè)計上要思考是否真的有必要這樣做盼玄,盡量避免秩冈。如果架構(gòu)需要這么設(shè)計爆阶,那么此對象的生命周期你有責(zé)任管理起來萌庆。
避免 override finalize()
1溶褪、finalize 方法被執(zhí)行的時間不確定,不能依賴與它來釋放緊缺的資源践险。時間不確定的原因是:
虛擬機(jī)調(diào)用GC的時間不確定
Finalize daemon線程被調(diào)度到的時間不確定
2猿妈、finalize 方法只會被執(zhí)行一次,即使對象被復(fù)活巍虫,如果已經(jīng)執(zhí)行過了 finalize 方法彭则,再次被 GC 時也不會再執(zhí)行了,原因是:
含有 finalize 方法的 object 是在 new 的時候由虛擬機(jī)生成了一個 finalize reference 在來引用到該Object的占遥,而在 finalize 方法執(zhí)行的時候俯抖,該 object 所對應(yīng)的 finalize Reference 會被釋放掉,即使在這個時候把該 object 復(fù)活(即用強(qiáng)引用引用住該 object )瓦胎,再第二次被 GC 的時候由于沒有了 finalize reference 與之對應(yīng)芬萍,所以 finalize 方法不會再執(zhí)行。
3搔啊、含有Finalize方法的object需要至少經(jīng)過兩輪GC才有可能被釋放柬祠。
資源未關(guān)閉造成的內(nèi)存泄漏
對于使用了BraodcastReceiver,ContentObserver负芋,F(xiàn)ile漫蛔,游標(biāo) Cursor,Stream旧蛾,Bitmap等資源的使用惩猫,應(yīng)該在Activity銷毀時及時關(guān)閉或者注銷,否則這些資源將不會被回收蚜点,造成內(nèi)存泄漏轧房。
一些不良代碼造成的內(nèi)存壓力
有些代碼并不造成內(nèi)存泄露,但是它們绍绘,或是對沒使用的內(nèi)存沒進(jìn)行有效及時的釋放奶镶,或是沒有有效的利用已有的對象而是頻繁的申請新內(nèi)存。
比如:
Bitmap 沒調(diào)用 recycle()方法陪拘,對于 Bitmap 對象在不使用時,我們應(yīng)該先調(diào)用 recycle() 釋放內(nèi)存厂镇,然后才它設(shè)置為 null. 因為加載 Bitmap 對象的內(nèi)存空間,一部分是 java 的左刽,一部分 C 的(因為 Bitmap 分配的底層是通過 JNI 調(diào)用的 )捺信。 而這個 recyle() 就是針對 C 部分的內(nèi)存釋放。
構(gòu)造 Adapter 時,沒有使用緩存的 convertView ,每次都在創(chuàng)建新的 converView迄靠。這里推薦使用 ViewHolder秒咨。
總結(jié)
對 Activity 等組件的引用應(yīng)該控制在 Activity 的生命周期之內(nèi); 如果不能就考慮使用 getApplicationContext 或者 getApplication掌挚,以避免 Activity 被外部長生命周期的對象引用而泄露雨席。
盡量不要在靜態(tài)變量或者靜態(tài)內(nèi)部類中使用非靜態(tài)外部成員變量(包括context ),即使要使用吠式,也要考慮適時把外部成員變量置空陡厘;也可以在內(nèi)部類中使用弱引用來引用外部類的變量。
對于生命周期比Activity長的內(nèi)部類對象特占,并且內(nèi)部類中使用了外部類的成員變量糙置,可以這樣做避免內(nèi)存泄漏:
將內(nèi)部類改為靜態(tài)內(nèi)部類
靜態(tài)內(nèi)部類中使用弱引用來引用外部類的成員變量
Handler 的持有的引用對象最好使用弱引用,資源釋放時也可以清空 Handler 里面的消息是目。比如在 Activity onStop 或者 onDestroy 的時候谤饭,取消掉該 Handler 對象的 Message和 Runnable.
在 Java 的實現(xiàn)過程中,也要考慮其對象釋放胖笛,最好的方法是在不使用某對象時网持,顯式地將此對象賦值為 null,比如使用完Bitmap 后先調(diào)用 recycle()长踊,再賦為null,清空對圖片等資源有直接引用或者間接引用的數(shù)組(使用 array.clear() ; array = null)等功舀,最好遵循誰創(chuàng)建誰釋放的原則。
正確關(guān)閉資源身弊,對于使用了BraodcastReceiver辟汰,ContentObserver,F(xiàn)ile阱佛,游標(biāo) Cursor帖汞,Stream,Bitmap等資源的使用凑术,應(yīng)該在Activity銷毀時及時關(guān)閉或者注銷翩蘸。
保持對對象生命周期的敏感,特別注意單例淮逊、靜態(tài)對象催首、全局性集合等的生命周期。#Android 內(nèi)存泄漏總結(jié)
內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題泄鹏。內(nèi)存泄漏大家都不陌生了郎任,簡單粗俗的講,就是該被釋放的對象沒有釋放备籽,一直被某個或某些實例所持有卻不再被使用導(dǎo)致 GC 不能回收舶治。最近自己閱讀了大量相關(guān)的文檔資料,打算做個 總結(jié) 沉淀下來跟大家一起分享和學(xué)習(xí),也給自己一個警示霉猛,以后 coding 時怎么避免這些情況尺锚,提高應(yīng)用的體驗和質(zhì)量。
我會從 java 內(nèi)存泄漏的基礎(chǔ)知識開始韩脏,并通過具體例子來說明 Android 引起內(nèi)存泄漏的各種原因缩麸,以及如何利用工具來分析應(yīng)用內(nèi)存泄漏铸磅,最后再做總結(jié)赡矢。
Java 內(nèi)存分配策略
Java 程序運(yùn)行時的內(nèi)存分配策略有三種,分別是靜態(tài)分配,棧式分配,和堆式分配,對應(yīng)的阅仔,三種存儲策略使用的內(nèi)存空間主要分別是靜態(tài)存儲區(qū)(也稱方法區(qū))吹散、棧區(qū)和堆區(qū)。
靜態(tài)存儲區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù)八酒、全局 static 數(shù)據(jù)和常量空民。這塊內(nèi)存在程序編譯時就已經(jīng)分配好,并且在程序整個運(yùn)行期間都存在羞迷。
棧區(qū) :當(dāng)方法被執(zhí)行時界轩,方法體內(nèi)的局部變量(其中包括基礎(chǔ)數(shù)據(jù)類型、對象的引用)都在棧上創(chuàng)建衔瓮,并在方法執(zhí)行結(jié)束時這些局部變量所持有的內(nèi)存將會自動被釋放浊猾。因為棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高热鞍,但是分配的內(nèi)存容量有限葫慎。
堆區(qū) : 又稱動態(tài)內(nèi)存分配,通常就是指在程序運(yùn)行時直接 new 出來的內(nèi)存薇宠,也就是對象的實例偷办。這部分內(nèi)存在不使用時將會由 Java 垃圾回收器來負(fù)責(zé)回收。
棧與堆的區(qū)別:
在方法體內(nèi)定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內(nèi)存中分配的澄港。當(dāng)在一段方法塊中定義一個變量時椒涯,Java 就會在棧中為該變量分配內(nèi)存空間,當(dāng)超過該變量的作用域后回梧,該變量也就無效了废岂,分配給它的內(nèi)存空間也將被釋放掉,該內(nèi)存空間可以被重新使用漂辐。
堆內(nèi)存用來存放所有由 new 創(chuàng)建的對象(包括該對象其中的所有成員變量)和數(shù)組泪喊。在堆中分配的內(nèi)存,將由 Java 垃圾回收器來自動管理髓涯。在堆中產(chǎn)生了一個數(shù)組或者對象后袒啼,還可以在棧中定義一個特殊的變量,這個變量的取值等于數(shù)組或者對象在堆內(nèi)存中的首地址,這個特殊的變量就是我們上面說的引用變量蚓再。我們可以通過這個引用變量來訪問堆中的對象或者數(shù)組滑肉。
舉個例子:
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 指向的對象是存在于堆上的摘仅。
mSample3 指向的對象實體存放在堆上靶庙,包括這個對象的所有成員變量 s1 和 mSample1,而它自己存在于棧中娃属。
結(jié)論:
局部變量的基本數(shù)據(jù)類型和引用存儲于棧中六荒,引用的對象實體存儲于堆中》耍—— 因為它們屬于方法中的變量掏击,生命周期隨方法而結(jié)束。
成員變量全部存儲與堆中(包括基本數(shù)據(jù)類型秩铆,引用和引用的對象實體)—— 因為它們屬于類砚亭,類對象終究是要被new出來使用的。
了解了 Java 的內(nèi)存分配之后殴玛,我們再來看看 Java 是怎么管理內(nèi)存的捅膘。
Java是如何管理內(nèi)存
Java的內(nèi)存管理就是對象的分配和釋放問題。在 Java 中滚粟,程序員需要通過關(guān)鍵字 new 為每個對象申請內(nèi)存空間 (基本類型除外)寻仗,所有的對象都在堆 (Heap)中分配空間。另外坦刀,對象的釋放是由 GC 決定和執(zhí)行的愧沟。在 Java 中,內(nèi)存的分配是由程序完成的鲤遥,而內(nèi)存的釋放是由 GC 完成的沐寺,這種收支兩條線的方法確實簡化了程序員的工作。但同時盖奈,它也加重了JVM的工作混坞。這也是 Java 程序運(yùn)行速度較慢的原因之一。因為钢坦,GC 為了能夠正確釋放對象究孕,GC 必須監(jiān)控每一個對象的運(yùn)行狀態(tài),包括對象的申請爹凹、引用厨诸、被引用、賦值等禾酱,GC 都需要進(jìn)行監(jiān)控微酬。
監(jiān)視對象狀態(tài)是為了更加準(zhǔn)確地绘趋、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用颗管。
為了更好理解 GC 的工作原理陷遮,我們可以將對象考慮為有向圖的頂點,將引用關(guān)系考慮為圖的有向邊垦江,有向邊從引用者指向被引對象帽馋。另外,每個線程對象可以作為一個圖的起始頂點比吭,例如大多程序從 main 進(jìn)程開始執(zhí)行绽族,那么該圖就是以 main 進(jìn)程頂點開始的一棵根樹。在這個有向圖中梗逮,根頂點可達(dá)的對象都是有效對象项秉,GC將不回收這些對象绣溜。如果某個對象 (連通子圖)與這個根頂點不可達(dá)(注意慷彤,該圖為有向圖),那么我們認(rèn)為這個(這些)對象不再被引用怖喻,可以被 GC 回收底哗。
以下,我們舉一個例子說明如何用有向圖表示內(nèi)存管理锚沸。對于程序的每一個時刻跋选,我們都有一個有向圖表示JVM的內(nèi)存分配情況。以下右圖哗蜈,就是左邊程序運(yùn)行到第6行的示意圖前标。
Java使用有向圖的方式進(jìn)行內(nèi)存管理,可以消除引用循環(huán)的問題距潘,例如有三個對象炼列,相互引用,只要它們和根進(jìn)程不可達(dá)的音比,那么GC也是可以回收它們的俭尖。這種方式的優(yōu)點是管理內(nèi)存的精度很高,但是效率較低洞翩。另外一種常用的內(nèi)存管理技術(shù)是使用計數(shù)器稽犁,例如COM模型采用計數(shù)器方式管理構(gòu)件,它與有向圖相比骚亿,精度行低(很難處理循環(huán)引用的問題)已亥,但執(zhí)行效率很高。
什么是Java中的內(nèi)存泄露
在Java中来屠,內(nèi)存泄漏就是存在一些被分配的對象虑椎,這些對象有下面兩個特點秫舌,首先,這些對象是可達(dá)的绣檬,即在有向圖中足陨,存在通路可以與其相連;其次娇未,這些對象是無用的墨缘,即程序以后不會再使用這些對象。如果對象滿足這兩個條件零抬,這些對象就可以判定為Java中的內(nèi)存泄漏镊讼,這些對象不會被GC所回收,然而它卻占用內(nèi)存平夜。
在C++中蝶棋,內(nèi)存泄漏的范圍更大一些。有些對象被分配了內(nèi)存空間忽妒,然后卻不可達(dá)玩裙,由于C++中沒有GC,這些內(nèi)存將永遠(yuǎn)收不回來段直。在Java中吃溅,這些不可達(dá)的對象都由GC負(fù)責(zé)回收,因此程序員不需要考慮這部分的內(nèi)存泄露鸯檬。
通過分析决侈,我們得知,對于C++喧务,程序員需要自己管理邊和頂點赖歌,而對于Java程序員只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式功茴,Java提高了編程的效率庐冯。
因此,通過以上分析痊土,我們知道在Java中也有內(nèi)存泄漏肄扎,但范圍比C++要小一些。因為Java從語言上保證赁酝,任何對象都是可達(dá)的犯祠,所有的不可達(dá)對象都由GC管理。
對于程序員來說酌呆,GC基本是透明的衡载,不可見的。雖然隙袁,我們只有幾個函數(shù)可以訪問GC痰娱,例如運(yùn)行GC的函數(shù)System.gc()弃榨,但是根據(jù)Java語言規(guī)范定義, 該函數(shù)不保證JVM的垃圾收集器一定會執(zhí)行梨睁。因為鲸睛,不同的JVM實現(xiàn)者可能使用不同的算法管理GC。通常坡贺,GC的線程的優(yōu)先級別較低官辈。JVM調(diào)用GC的策略也有很多種,有的是內(nèi)存使用到達(dá)一定程度時遍坟,GC才開始工作拳亿,也有定時執(zhí)行的,有的是平緩執(zhí)行GC愿伴,有的是中斷式執(zhí)行GC肺魁。但通常來說,我們不需要關(guān)心這些隔节。除非在一些特定的場合鹅经,GC的執(zhí)行影響應(yīng)用程序的性能,例如對于基于Web的實時系統(tǒng)官帘,如網(wǎng)絡(luò)游戲等瞬雹,用戶不希望GC突然中斷應(yīng)用程序執(zhí)行而進(jìn)行垃圾回收,那么我們需要調(diào)整GC的參數(shù)刽虹,讓GC能夠通過平緩的方式釋放內(nèi)存,例如將垃圾回收分解為一系列的小步驟執(zhí)行呢诬,Sun提供的HotSpot JVM就支持這一特性涌哲。
同樣給出一個 Java 內(nèi)存泄漏的典型例子,
Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
在這個例子中尚镰,我們循環(huán)申請Object對象阀圾,并將所申請的對象放入一個 Vector 中,如果我們僅僅釋放引用本身狗唉,那么 Vector 仍然引用該對象初烘,所以這個對象對 GC 來說是不可回收的。因此分俯,如果對象加入到Vector 后肾筐,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對象設(shè)置為 null缸剪。
詳細(xì)Java中的內(nèi)存泄漏
1.Java內(nèi)存回收機(jī)制
不論哪種語言的內(nèi)存分配方式吗铐,都需要返回所分配內(nèi)存的真實地址,也就是返回一個指針到內(nèi)存塊的首地址杏节。Java中對象是采用new或者反射的方法創(chuàng)建的唬渗,這些對象的創(chuàng)建都是在堆(Heap)中分配的典阵,所有對象的回收都是由Java虛擬機(jī)通過垃圾回收機(jī)制完成的。GC為了能夠正確釋放對象镊逝,會監(jiān)控每個對象的運(yùn)行狀況壮啊,對他們的申請、引用撑蒜、被引用他巨、賦值等狀況進(jìn)行監(jiān)控,Java會使用有向圖的方法進(jìn)行管理內(nèi)存减江,實時監(jiān)控對象是否可以達(dá)到染突,如果不可到達(dá),則就將其回收辈灼,這樣也可以消除引用循環(huán)的問題份企。在Java語言中,判斷一個內(nèi)存空間是否符合垃圾收集標(biāo)準(zhǔn)有兩個:一個是給對象賦予了空值null巡莹,以下再沒有調(diào)用過司志,另一個是給對象賦予了新值,這樣重新分配了內(nèi)存空間降宅。
2.Java內(nèi)存泄漏引起的原因
內(nèi)存泄漏是指無用對象(不再使用的對象)持續(xù)占有內(nèi)存或無用對象的內(nèi)存得不到及時釋放骂远,從而造成內(nèi)存空間的浪費稱為內(nèi)存泄漏。內(nèi)存泄露有時不嚴(yán)重且不易察覺腰根,這樣開發(fā)者就不知道存在內(nèi)存泄露激才,但有時也會很嚴(yán)重,會提示你Out of memory额嘿。j
Java內(nèi)存泄漏的根本原因是什么呢瘸恼?長生命周期的對象持有短生命周期對象的引用就很可能發(fā)生內(nèi)存泄漏,盡管短生命周期對象已經(jīng)不再需要册养,但是因為長生命周期持有它的引用而導(dǎo)致不能被回收东帅,這就是Java中內(nèi)存泄漏的發(fā)生場景。具體主要有如下幾大類:
1球拦、靜態(tài)集合類引起內(nèi)存泄漏:
像HashMap靠闭、Vector等的使用最容易出現(xiàn)內(nèi)存泄露,這些靜態(tài)變量的生命周期和應(yīng)用程序一致坎炼,他們所引用的所有的對象Object也不能被釋放愧膀,因為他們也將一直被Vector等引用著。
例如
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
在這個例子中点弯,循環(huán)申請Object 對象扇调,并將所申請的對象放入一個Vector 中,如果僅僅釋放引用本身(o=null)抢肛,那么Vector 仍然引用該對象狼钮,所以這個對象對GC 來說是不可回收的碳柱。因此,如果對象加入到Vector 后熬芜,還必須從Vector 中刪除莲镣,最簡單的方法就是將Vector對象設(shè)置為null。
2涎拉、當(dāng)集合里面的對象屬性被修改后瑞侮,再調(diào)用remove()方法時不起作用。
例如:
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()+" 個元素!"); //結(jié)果:總共有:3 個元素!
p3.setAge(2); //修改p3的年齡,此時p3元素對應(yīng)的hashcode值發(fā)生改變
set.remove(p3); //此時remove不掉鼓拧,造成內(nèi)存泄漏
set.add(p3); //重新添加半火,居然添加成功
System.out.println("總共有:"+set.size()+" 個元素!"); //結(jié)果:總共有:4 個元素!
for (Person person : set)
{
System.out.println(person);
}
}
3、監(jiān)聽器
在java 編程中季俩,我們都需要和監(jiān)聽器打交道钮糖,通常一個應(yīng)用當(dāng)中會用到很多監(jiān)聽器,我們會調(diào)用一個控件的諸如addXXXListener()等方法來增加監(jiān)聽器酌住,但往往在釋放對象的時候卻沒有記住去刪除這些監(jiān)聽器店归,從而增加了內(nèi)存泄漏的機(jī)會。
4酪我、各種連接
比如數(shù)據(jù)庫連接(dataSourse.getConnection())消痛,網(wǎng)絡(luò)連接(socket)和io連接,除非其顯式的調(diào)用了其close()方法將其連接關(guān)閉都哭,否則是不會自動被GC 回收的秩伞。對于Resultset 和Statement 對象可以不進(jìn)行顯式回收,但Connection 一定要顯式回收质涛,因為Connection 在任何時候都無法自動回收稠歉,而Connection一旦回收,Resultset 和Statement 對象就會立即為NULL汇陆。但是如果使用連接池,情況就不一樣了带饱,除了要顯式地關(guān)閉連接毡代,還必須顯式地關(guān)閉Resultset Statement 對象(關(guān)閉其中一個,另外一個也會關(guān)閉)勺疼,否則就會造成大量的Statement 對象無法釋放教寂,從而引起內(nèi)存泄漏。這種情況下一般都會在try里面去的連接执庐,在finally里面釋放連接酪耕。
5、內(nèi)部類和外部模塊的引用
內(nèi)部類的引用是比較容易遺忘的一種轨淌,而且一旦沒釋放可能導(dǎo)致一系列的后繼類對象沒有釋放迂烁。此外程序員還要小心外部模塊不經(jīng)意的引用看尼,例如程序員A 負(fù)責(zé)A 模塊,調(diào)用了B 模塊的一個方法如:
public void registerMsg(Object b);
這種調(diào)用就要非常小心了盟步,傳入了一個對象藏斩,很可能模塊B就保持了對該對象的引用,這時候就需要注意模塊B 是否提供相應(yīng)的操作去除引用却盘。
6狰域、單例模式
不正確使用單例模式是引起內(nèi)存泄漏的一個常見問題,單例對象在初始化后將在JVM的整個生命周期中存在(以靜態(tài)變量的方式)黄橘,如果單例對象持有外部的引用兆览,那么這個對象將不能被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模式,它持有一個A對象的引用描孟,而這個A類的對象將不能被回收驶睦。想象下如果A是個比較復(fù)雜的對象或者集合類型會發(fā)生什么情況
Android中常見的內(nèi)存泄漏匯總
集合類泄漏
集合類如果僅僅有添加元素的方法,而沒有相應(yīng)的刪除機(jī)制匿醒,導(dǎo)致內(nèi)存被占用场航。如果這個集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它)廉羔,那么沒有相應(yīng)的刪除機(jī)制溉痢,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。比如上面的典型例子就是其中一種情況憋他,當(dāng)然實際上我們在項目中肯定不會寫這么 2B 的代碼孩饼,但稍不注意還是很容易出現(xiàn)這種情況,比如我們都喜歡通過 HashMap 做一些緩存之類的事竹挡,這種情況就要多留一些心眼镀娶。
單例造成的內(nèi)存泄漏
由于單例的靜態(tài)特性使得其生命周期跟應(yīng)用的生命周期一樣長,所以如果使用不恰當(dāng)?shù)脑捑竞保苋菀自斐蓛?nèi)存泄漏梯码。比如下面一個典型的例子,
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;
}
}
這是一個普通的單例模式好啰,當(dāng)創(chuàng)建這個單例的時候轩娶,由于需要傳入一個Context,所以這個Context的生命周期的長短至關(guān)重要:
1框往、如果此時傳入的是 Application 的 Context鳄抒,因為 Application 的生命周期就是整個應(yīng)用的生命周期,所以這將沒有任何問題。
2许溅、如果此時傳入的是 Activity 的 Context瓤鼻,當(dāng)這個 Context 所對應(yīng)的 Activity 退出時,由于該 Context 的引用被單例對象所持有闹司,其生命周期等于整個應(yīng)用程序的生命周期,所以當(dāng)前 Activity 退出時它的內(nèi)存并不會被回收游桩,這就造成泄漏了牲迫。
正確的方式應(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 中添加一個靜態(tài)方法借卧,getContext() 返回 Application 的 context盹憎,
...
context = getApplicationContext();
...
/**
* 獲取全局的context
* @return 返回全局context對象
*/
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)實例造成的內(nèi)存泄漏
有的時候我們可能會在啟動頻繁的Activity中,為了避免重復(fù)創(chuàng)建相同的數(shù)據(jù)資源铐刘,可能會出現(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)建了一個非靜態(tài)內(nèi)部類的單例陪每,每次啟動Activity時都會使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復(fù)創(chuàng)建镰吵,不過這種寫法卻會造成內(nèi)存泄漏檩禾,因為非靜態(tài)內(nèi)部類默認(rèn)會持有外部類的引用,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個靜態(tài)的實例疤祭,該實例的生命周期和應(yīng)用的一樣長郎汪,這就導(dǎo)致了該靜態(tài)實例一直會持有該Activity的引用切威,導(dǎo)致Activity的內(nèi)存資源不能正成■辏回收拗秘。正確的做法為:
將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個單例,如果需要使用Context草穆,請按照上面推薦的使用Application 的 Context灌灾。當(dāng)然,Application 的 context 不是萬能的悲柱,所以也不能隨便亂用锋喜,對于有些地方則必須使用 Activity 的 Context,對于Application豌鸡,Service跑芳,Activity三者的Context的應(yīng)用場景如下:
其中: NO1表示 Application 和 Service 可以啟動一個 Activity,不過需要創(chuàng)建一個新的 task 任務(wù)隊列直颅。而對于 Dialog 而言,只有在 Activity 中才能創(chuàng)建
匿名內(nèi)部類
android開發(fā)經(jīng)常會繼承實現(xiàn)Activity/Fragment/View怀樟,此時如果你使用了匿名類功偿,并被異步線程持有了,那要小心了,如果沒有任何措施這樣一定會導(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)行時這兩個引用的內(nèi)存:
可以看到,ref1沒什么特別的吨瞎。
但ref2這個匿名類的實現(xiàn)對象里面多了一個引用:
this$0這個引用指向MainActivity.this痹兜,也就是說當(dāng)前的MainActivity實例會被ref2持有,如果將這個引用再傳入一個異步線程颤诀,此線程和此Acitivity生命周期不一致的時候字旭,就造成了Activity的泄露。
Handler 造成的內(nèi)存泄漏
Handler 的使用造成的內(nèi)存泄漏問題應(yīng)該說是最為常見了崖叫,很多時候我們?yōu)榱吮苊?ANR 而不在主線程進(jìn)行耗時操作遗淳,在處理網(wǎng)絡(luò)任務(wù)或者封裝一些請求回調(diào)等api都借助Handler來處理,但 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 是不一致的弃甥。因此這種實現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導(dǎo)致無法正確釋放堕油。
舉個例子:
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 中聲明了一個延遲10分鐘執(zhí)行的消息 Message潘飘,mLeakyHandler 將其 push 進(jìn)了消息隊列 MessageQueue 里。當(dāng)該 Activity 被 finish() 掉時掉缺,延遲執(zhí)行任務(wù)的 Message 還會繼續(xù)存在于主線程中卜录,它持有該 Activity 的 Handler 引用,所以此時 finish() 掉的 Activity 就不會被回收了從而造成內(nèi)存泄漏(因 Handler 為非靜態(tài)內(nèi)部類眶明,它會持有外部類的引用艰毒,在這里就是指 SampleActivity)。
修復(fù)方法:在 Activity 中避免使用非靜態(tài)內(nèi)部類搜囱,比如上面我們將 Handler 聲明為靜態(tài)的丑瞧,則其存活期跟 Activity 的生命周期就無關(guān)了。同時通過弱引用的方式引入 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,所以這里就簡單的說一下 Java 對象的幾種引用類型。
Java對引用的分類有 Strong reference, SoftReference, WeakReference, PhatomReference 四種获雕。
在Android應(yīng)用的開發(fā)中薄腻,為了防止內(nèi)存溢出,在處理一些占用內(nèi)存大而且聲明周期較長的對象時候届案,可以盡量應(yīng)用軟引用和弱引用技術(shù)庵楷。
軟/弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對象被垃圾回收器回收楣颠,Java虛擬機(jī)就會把這個軟引用加入到與之關(guān)聯(lián)的引用隊列中尽纽。利用這個隊列可以得知被回收的軟/弱引用的對象列表,從而為緩沖器清除已失效的軟/弱引用球碉。
假設(shè)我們的應(yīng)用會用到大量的默認(rèn)圖片蜓斧,比如應(yīng)用中有默認(rèn)的頭像,默認(rèn)游戲圖標(biāo)等等睁冬,這些圖片很多地方會用到挎春。如果每次都去讀取圖片,由于讀取文件需要硬件操作豆拨,速度較慢直奋,會導(dǎo)致性能較低。所以我們考慮將圖片緩存起來施禾,需要的時候直接從內(nèi)存中讀取脚线。但是,由于圖片占用內(nèi)存空間比較大弥搞,緩存很多圖片需要很多的內(nèi)存邮绿,就可能比較容易發(fā)生OutOfMemory異常。這時攀例,我們可以考慮使用軟/弱引用技術(shù)來避免這個問題發(fā)生船逮。以下就是高速緩沖器的雛形:
首先定義一個HashMap,保存軟引用對象粤铭。
private Map <String, SoftReference<Bitmap>> imageCache = new HashMap <String, SoftReference<Bitmap>> ();
再來定義一個方法挖胃,保存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)建一個靜態(tài)Handler內(nèi)部類拂到,然后對 Handler 持有的對象使用弱引用痪署,這樣在回收時也可以回收 Handler 持有的對象,但是這樣做雖然避免了 Activity 泄漏兄旬,不過 Looper 線程的消息隊列中還是可能會有待處理的消息狼犯,所以我們在 Activity 的 Destroy 時或者 Stop 時應(yīng)該移除消息隊列 MessageQueue 中的消息。
下面幾個方法都可以移除 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领铐,那我們都知道其生命周期將與整個app進(jìn)程生命周期一樣悯森。
這會導(dǎo)致一系列問題,如果你的app進(jìn)程設(shè)計上是長駐內(nèi)存的绪撵,那即使app切到后臺瓢姻,這部分內(nèi)存也不會被釋放。按照現(xiàn)在手機(jī)app內(nèi)存管理機(jī)制音诈,占內(nèi)存較大的后臺進(jìn)程將優(yōu)先回收幻碱,yi'wei如果此app做過進(jìn)程互保保活细溅,那會造成app在后臺頻繁重啟褥傍。當(dāng)手機(jī)安裝了你參與開發(fā)的app以后一夜時間手機(jī)被消耗空了電量、流量谒兄,你的app不得不被用戶卸載或者靜默摔桦。
這里修復(fù)的方法是:
不要在類初始時初始化靜態(tài)成員〕衅#可以考慮lazy初始化邻耕。
架構(gòu)設(shè)計上要思考是否真的有必要這樣做,盡量避免燕鸽。如果架構(gòu)需要這么設(shè)計兄世,那么此對象的生命周期你有責(zé)任管理起來。
避免 override finalize()
1啊研、finalize 方法被執(zhí)行的時間不確定御滩,不能依賴與它來釋放緊缺的資源鸥拧。時間不確定的原因是:
虛擬機(jī)調(diào)用GC的時間不確定
Finalize daemon線程被調(diào)度到的時間不確定
2、finalize 方法只會被執(zhí)行一次削解,即使對象被復(fù)活富弦,如果已經(jīng)執(zhí)行過了 finalize 方法,再次被 GC 時也不會再執(zhí)行了氛驮,原因是:
含有 finalize 方法的 object 是在 new 的時候由虛擬機(jī)生成了一個 finalize reference 在來引用到該Object的腕柜,而在 finalize 方法執(zhí)行的時候,該 object 所對應(yīng)的 finalize Reference 會被釋放掉矫废,即使在這個時候把該 object 復(fù)活(即用強(qiáng)引用引用住該 object )盏缤,再第二次被 GC 的時候由于沒有了 finalize reference 與之對應(yīng),所以 finalize 方法不會再執(zhí)行蓖扑。
3唉铜、含有Finalize方法的object需要至少經(jīng)過兩輪GC才有可能被釋放。
資源未關(guān)閉造成的內(nèi)存泄漏
對于使用了BraodcastReceiver律杠,ContentObserver潭流,F(xiàn)ile,游標(biāo) Cursor俩功,Stream幻枉,Bitmap等資源的使用,應(yīng)該在Activity銷毀時及時關(guān)閉或者注銷诡蜓,否則這些資源將不會被回收熬甫,造成內(nèi)存泄漏。
一些不良代碼造成的內(nèi)存壓力
有些代碼并不造成內(nèi)存泄露蔓罚,但是它們椿肩,或是對沒使用的內(nèi)存沒進(jìn)行有效及時的釋放,或是沒有有效的利用已有的對象而是頻繁的申請新內(nèi)存豺谈。
比如:
Bitmap 沒調(diào)用 recycle()方法郑象,對于 Bitmap 對象在不使用時,我們應(yīng)該先調(diào)用 recycle() 釋放內(nèi)存,然后才它設(shè)置為 null. 因為加載 Bitmap 對象的內(nèi)存空間茬末,一部分是 java 的厂榛,一部分 C 的(因為 Bitmap 分配的底層是通過 JNI 調(diào)用的 )。 而這個 recyle() 就是針對 C 部分的內(nèi)存釋放丽惭。
構(gòu)造 Adapter 時击奶,沒有使用緩存的 convertView ,每次都在創(chuàng)建新的 converView。這里推薦使用 ViewHolder责掏。
總結(jié)
對 Activity 等組件的引用應(yīng)該控制在 Activity 的生命周期之內(nèi)柜砾; 如果不能就考慮使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部長生命周期的對象引用而泄露换衬。
盡量不要在靜態(tài)變量或者靜態(tài)內(nèi)部類中使用非靜態(tài)外部成員變量(包括context )痰驱,即使要使用证芭,也要考慮適時把外部成員變量置空;也可以在內(nèi)部類中使用弱引用來引用外部類的變量担映。
對于生命周期比Activity長的內(nèi)部類對象废士,并且內(nèi)部類中使用了外部類的成員變量,可以這樣做避免內(nèi)存泄漏:
將內(nèi)部類改為靜態(tài)內(nèi)部類
靜態(tài)內(nèi)部類中使用弱引用來引用外部類的成員變量
Handler 的持有的引用對象最好使用弱引用另萤,資源釋放時也可以清空 Handler 里面的消息湃密。比如在 Activity onStop 或者 onDestroy 的時候,取消掉該 Handler 對象的 Message和 Runnable.
在 Java 的實現(xiàn)過程中四敞,也要考慮其對象釋放,最好的方法是在不使用某對象時拔妥,顯式地將此對象賦值為 null忿危,比如使用完Bitmap 后先調(diào)用 recycle(),再賦為null,清空對圖片等資源有直接引用或者間接引用的數(shù)組(使用 array.clear() ; array = null)等没龙,最好遵循誰創(chuàng)建誰釋放的原則铺厨。
正確關(guān)閉資源,對于使用了BraodcastReceiver硬纤,ContentObserver解滓,F(xiàn)ile,游標(biāo) Cursor筝家,Stream洼裤,Bitmap等資源的使用,應(yīng)該在Activity銷毀時及時關(guān)閉或者注銷溪王。
保持對對象生命周期的敏感腮鞍,特別注意單例、靜態(tài)對象莹菱、全局性集合等的生命周期移国。