java與android 內(nèi)存泄漏總結(jié)

內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏大家都不陌生了,簡單粗俗的講,就是該被釋放的對象沒有釋放,一直被某個或某些實例所持有卻不再被使用導(dǎo)致 GC 不能回收。最近自己閱讀了大量相關(guān)的文檔資料,打算做個總結(jié)沉淀下來跟大家一起分享和學習,也給自己一個警示,以后 coding 時怎么避免這些情況,提高應(yīng)用的體驗和質(zhì)量。

我會從 java 內(nèi)存泄漏的基礎(chǔ)知識開始,

并通過具體例子來說明 Android 引起內(nèi)存泄漏的各種原因,以及如何利用工具來分析應(yīng)用內(nèi)存泄漏,最后再做總結(jié)。

篇幅有些長,大家可以分幾節(jié)來看!

Java 內(nèi)存分配策略

Java 程序運行時的內(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)分配好,并且在程序整個運行期間都存在。

棧區(qū) :當方法被執(zhí)行時,方法體內(nèi)的局部變量都在棧上創(chuàng)建,并在方法執(zhí)行結(jié)束時這些局部變量所持有的內(nèi)存將會自動被釋放。因為棧內(nèi)存分配運算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。

堆區(qū) : 又稱動態(tài)內(nèi)存分配,通常就是指在程序運行時直接 new 出來的內(nèi)存。這部分內(nèi)存在不使用時將會由 Java 垃圾回收器來負責回收。

棧與堆的區(qū)別:

在方法體內(nèi)定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內(nèi)存中分配的。當在一段方法塊中定義一個變量時,Java 就會在棧中為該變量分配內(nèi)存空間,當超過該變量的作用域后,該變量也就無效了,分配給它的內(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 程序運行速度較慢的原因之一。因為,GC 為了能夠正確釋放對象,GC 必須監(jiān)控每一個對象的運行狀態(tài),包括對象的申請、引用、被引用、賦值等,GC 都需要進行監(jiān)控。

監(jiān)視對象狀態(tài)是為了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。

為了更好理解 GC 的工作原理,我們可以將對象考慮為有向圖的頂點,將引用關(guān)系考慮為圖的有向邊,有向邊從引用者指向被引對象。另外,每個線程對象可以作為一個圖的起始頂點,例如大多程序從 main 進程開始執(zhí)行,那么該圖就是以 main 進程頂點開始的一棵根樹。在這個有向圖中,根頂點可達的對象都是有效對象,GC將不回收這些對象。如果某個對象 (連通子圖)與這個根頂點不可達(注意,該圖為有向圖),那么我們認為這個(這些)對象不再被引用,可以被 GC 回收。

以下,我們舉一個例子說明如何用有向圖表示內(nèi)存管理。對于程序的每一個時刻,我們都有一個有向圖表示JVM的內(nèi)存分配情況。以下右圖,就是左邊程序運行到第6行的示意圖。

Java使用有向圖的方式進行內(nèi)存管理,可以消除引用循環(huán)的問題,例如有三個對象,相互引用,只要它們和根進程不可達的,那么GC也是可以回收它們的。這種方式的優(yōu)點是管理內(nèi)存的精度很高,但是效率較低。另外一種常用的內(nèi)存管理技術(shù)是使用計數(shù)器,例如COM模型采用計數(shù)器方式管理構(gòu)件,它與有向圖相比,精度行低(很難處理循環(huán)引用的問題),但執(zhí)行效率很高。

什么是Java中的內(nèi)存泄露

在Java中,內(nèi)存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;其次,這些對象是無用的,即程序以后不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的內(nèi)存泄漏,這些對象不會被GC所回收,然而它卻占用內(nèi)存。

在C++中,內(nèi)存泄漏的范圍更大一些。有些對象被分配了內(nèi)存空間,然后卻不可達,由于C++中沒有GC,這些內(nèi)存將永遠收不回來。在Java中,這些不可達的對象都由GC負責回收,因此程序員不需要考慮這部分的內(nèi)存泄露。

通過分析,我們得知,對于C++,程序員需要自己管理邊和頂點,而對于Java程序員只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式,Java提高了編程的效率。

因此,通過以上分析,我們知道在Java中也有內(nèi)存泄漏,但范圍比C++要小一些。因為Java從語言上保證,任何對象都是可達的,所有的不可達對象都由GC管理。

? ? 對于程序員來說,GC基本是透明的,不可見的。雖然,我們只有幾個函數(shù)可以訪問GC,例如運行GC的函數(shù)System.gc(),但是根據(jù)Java語言規(guī)范定義, 該函數(shù)不保證JVM的垃圾收集器一定會執(zhí)行。因為,不同的JVM實現(xiàn)者可能使用不同的算法管理GC。通常,GC的線程的優(yōu)先級別較低。JVM調(diào)用GC的策略也有很多種,有的是內(nèi)存使用到達一定程度時,GC才開始工作,也有定時執(zhí)行的,有的是平緩執(zhí)行GC,有的是中斷式執(zhí)行GC。但通常來說,我們不需要關(guān)心這些。除非在一些特定的場合,GC的執(zhí)行影響應(yīng)用程序的性能,例如對于基于Web的實時系統(tǒng),如網(wǎng)絡(luò)游戲等,用戶不希望GC突然中斷應(yīng)用程序執(zhí)行而進行垃圾回收,那么我們需要調(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。

Android中常見的內(nèi)存泄漏匯總

1.集合類泄漏

? ? 集合類如果僅僅有添加元素的方法,而沒有相應(yīng)的刪除機制,導(dǎo)致內(nèi)存被占用。如果這個集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它),那么沒有相應(yīng)的刪除機制,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。比如上面的典型例子就是其中一種情況,當然實際上我們在項目中肯定不會寫這么 2B 的代碼,但稍不注意還是很容易出現(xiàn)這種情況,比如我們都喜歡通過 HashMap 做一些緩存之類的事,這種情況就要多留一些心眼。

2.單例造成的內(nèi)存泄漏

由于單例的靜態(tài)特性使得其生命周期跟應(yī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;

? ? ? ? ? ?}}

這是一個普通的單例模式,當創(chuàng)建這個單例的時候,由于需要傳入一個Context,所以這個Context的生命周期的長短至關(guān)重要:

1、如果此時傳入的是 Application 的 Context,因為 Application 的生命周期就是整個應(yīng)用的生命周期,所以這將沒有任何問題。

2、如果此時傳入的是 Activity 的 Context,當這個 Context 所對應(yīng)的 Activity 退出時,由于該 Context 的引用被單例對象所持有,其生命周期等于整個應(yī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 都不用傳進來了:

在你的?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)部類默認會持有外部類的引用,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個靜態(tài)的實例,該實例的生命周期和應(yīng)用的一樣長,這就導(dǎo)致了該靜態(tài)實例一直會持有該Activity的引用,導(dǎo)致Activity的內(nèi)存資源不能正?;厥铡U_的做法為:

將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個單例,如果需要使用Context,請按照上面推薦的使用Application 的 Context。當然,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)部類。我們來看看運行時這兩個引用的內(nèi)存:

可以看到,ref1沒什么特別的。

但ref2這個匿名類的實現(xiàn)對象里面多了一個引用:

this$0這個引用指向MainActivity.this,也就是說當前的MainActivity實例會被ref2持有,如果將這個引用再傳入一個異步線程,此線程和此Acitivity生命周期不一致的時候,就造成了Activity的泄露。

Handler 造成的內(nèi)存泄漏

Handler 的使用造成的內(nèi)存泄漏問題應(yīng)該說是最為常見了,很多時候我們?yōu)榱吮苊?ANR 而不在主線程進行耗時操作,在處理網(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()?{

@Overridepublic?void?handleMessage(Message?msg)?{??//?...}

}

@Overrideprotected?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()?{?/.../?}

},?10006010);//?Go?back?to?the?previous?Activity.finish();

}

}

在該 SampleActivity 中聲明了一個延遲10分鐘執(zhí)行的消息 Message,mLeakyHandler 將其 push 進了消息隊列 MessageQueue 里。當該 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 傳進去,見下面代碼:

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?mActivity;

public?MyHandler(SampleActivity?activity)?{

mActivity?=?new?WeakReference(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,?10006010);????//?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虛擬機就會把這個軟引用加入到與之關(guān)聯(lián)的引用隊列中。利用這個隊列可以得知被回收的軟/弱引用的對象列表,從而為緩沖器清除已失效的軟/弱引用。

假設(shè)我們的應(yīng)用會用到大量的默認圖片,比如應(yīng)用中有默認的頭像,默認游戲圖標等等,這些圖片很多地方會用到。如果每次都去讀取圖片,由于讀取文件需要硬件操作,速度較慢,會導(dǎo)致性能較低。所以我們考慮將圖片緩存起來,需要的時候直接從內(nèi)存中讀取。但是,由于圖片占用內(nèi)存空間比較大,緩存很多圖片需要很多的內(nèi)存,就可能比較容易發(fā)生OutOfMemory異常。這時,我們可以考慮使用軟/弱引用技術(shù)來避免這個問題發(fā)生。以下就是高速緩沖器的雛形:

首先定義一個HashMap,保存軟引用對象。

private?Map?>?imageCache?=?new?HashMap?>?();

再來定義一個方法,保存Bitmap的軟引用到HashMap。

使用軟引用以后,在OutOfMemory異常發(fā)生之前,這些緩存的圖片資源的內(nèi)存空間可以被釋放掉的,從而避免內(nèi)存達到上限,避免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進程生命周期一樣。

這會導(dǎo)致一系列問題,如果你的app進程設(shè)計上是長駐內(nèi)存的,那即使app切到后臺,這部分內(nèi)存也不會被釋放。按照現(xiàn)在手機app內(nèi)存管理機制,占內(nèi)存較大的后臺進程將優(yōu)先回收,yi'wei如果此app做過進程互保保活,那會造成app在后臺頻繁重啟。當手機安裝了你參與開發(fā)的app以后一夜時間手機被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。

這里修復(fù)的方法是:

不要在類初始時初始化靜態(tài)成員??梢钥紤]lazy初始化。

架構(gòu)設(shè)計上要思考是否真的有必要這樣做,盡量避免。如果架構(gòu)需要這么設(shè)計,那么此對象的生命周期你有責任管理起來。

避免 override finalize()

1、finalize 方法被執(zhí)行的時間不確定,不能依賴與它來釋放緊缺的資源。時間不確定的原因是:

2、finalize 方法只會被執(zhí)行一次,即使對象被復(fù)活,如果已經(jīng)執(zhí)行過了 finalize 方法,再次被 GC 時也不會再執(zhí)行了,原因是:

含有 finalize 方法的 object 是在 new 的時候由虛擬機生成了一個 finalize reference 在來引用到該Object的,而在 finalize 方法執(zhí)行的時候,該 object 所對應(yīng)的 finalize Reference 會被釋放掉,即使在這個時候把該 object 復(fù)活(即用強引用引用住該 object ),再第二次被 GC 的時候由于沒有了 finalize reference 與之對應(yīng),所以 finalize 方法不會再執(zhí)行。

3、含有Finalize方法的object需要至少經(jīng)過兩輪GC才有可能被釋放。

詳情見這里深入分析過dalvik的代碼

虛擬機調(diào)用GC的時間不確定

Finalize daemon線程被調(diào)度到的時間不確定

資源未關(guān)閉造成的內(nèi)存泄漏

對于使用了BraodcastReceiver,ContentObserver,F(xiàn)ile,游標 Cursor,Stream,Bitmap等資源的使用,應(yīng)該在Activity銷毀時及時關(guān)閉或者注銷,否則這些資源將不會被回收,造成內(nèi)存泄漏。

一些不良代碼造成的內(nèi)存壓力

有些代碼并不造成內(nèi)存泄露,但是它們,或是對沒使用的內(nèi)存沒進行有效及時的釋放,或是沒有有效的利用已有的對象而是頻繁的申請新內(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。

工具分析

Java 內(nèi)存泄漏的分析工具有很多,但眾所周知的要數(shù) MAT(Memory Analysis Tools) 和 YourKit 了。由于篇幅問題,我這里就只對MAT的使用做一下介紹。-->MAT 的安裝

MAT分析heap的總內(nèi)存占用大小來初步判斷是否存在泄露

打開 DDMS 工具,在左邊 Devices 視圖頁面選中“Update Heap”圖標,然后在右邊切換到 Heap 視圖,點擊 Heap 視圖中的“Cause GC”按鈕,到此為止需檢測的進程就可以被監(jiān)視。

Heap視圖中部有一個Type叫做data object,即數(shù)據(jù)對象,也就是我們的程序中大量存在的類類型的對象。在data object一行中有一列是“Total Size”,其值就是當前進程中所有Java數(shù)據(jù)對象的內(nèi)存總量,一般情況下,這個值的大小決定了是否會有內(nèi)存泄漏。可以這樣判斷:

進入某應(yīng)用,不斷的操作該應(yīng)用,同時注意觀察data object的Total Size值,正常情況下Total Size值都會穩(wěn)定在一個有限的范圍內(nèi),也就是說由于程序中的的代碼良好,沒有造成對象不被垃圾回收的情況。

所以說雖然我們不斷的操作會不斷的生成很多對象,而在虛擬機不斷的進行GC的過程中,這些對象都被回收了,內(nèi)存占用量會會落到一個穩(wěn)定的水平;反之如果代碼中存在沒有釋放對象引用的情況,則data object的Total Size值在每次GC后不會有明顯的回落。隨著操作次數(shù)的增多Total Size的值會越來越大,直到到達一個上限后導(dǎo)致進程被殺掉。

MAT分析hprof來定位內(nèi)存泄露的原因所在

這是出現(xiàn)內(nèi)存泄露后使用MAT進行問題定位的有效手段。

A)Dump出內(nèi)存泄露當時的內(nèi)存鏡像hprof,分析懷疑泄露的類:

B)分析持有此類對象引用的外部對象

C)分析這些持有引用的對象的GC路徑

D)逐個分析每個對象的GC路徑是否正常

從這個路徑可以看出是一個antiRadiationUtil工具類對象持有了MainActivity的引用導(dǎo)致MainActivity無法釋放。此時就要進入代碼分析此時antiRadiationUtil的引用持有是否合理(如果antiRadiationUtil持有了MainActivity的context導(dǎo)致節(jié)目退出后MainActivity無法銷毀,那一般都屬于內(nèi)存泄露了)。

MAT對比操作前后的hprof來定位內(nèi)存泄露的根因所在

為查找內(nèi)存泄漏,通常需要兩個 Dump結(jié)果作對比,打開 Navigator History面板,將兩個表的 Histogram結(jié)果都添加到 Compare Basket中去

A) 第一個HPROF 文件(usingFile > Open Heap Dump ).

B)打開Histogram view.

C)在NavigationHistory view里 (如果看不到就從Window >show view>MAT- Navigation History ), 右擊histogram然后選擇Add to Compare Basket .

D)打開第二個HPROF 文件然后重做步驟2和3.

E)切換到Compare Basket view, 然后點擊Compare the Results (視圖右上角的紅色”!”圖標)。

F)分析對比結(jié)果

可以看出兩個hprof的數(shù)據(jù)對象對比結(jié)果。

通過這種方式可以快速定位到操作前后所持有的對象增量,從而進一步定位出當前操作導(dǎo)致內(nèi)存泄露的具體原因是泄露了什么數(shù)據(jù)對象。

注意:

如果是用 MAT Eclipse 插件獲取的 Dump文件,不需要經(jīng)過轉(zhuǎn)換則可在MAT中打開,Adt會自動進行轉(zhuǎn)換。

而手機SDk Dump 出的文件要經(jīng)過轉(zhuǎn)換才能被 MAT識別,Android SDK提供了這個工具 hprof-conv (位于 sdk/tools下)

首先,要通過控制臺進入到你的 android sdk tools 目錄下執(zhí)行以下命令:

./hprof-conv xxx-a.hprof xxx-b.hprof

例如 hprof-conv input.hprof out.hprof

此時才能將out.hprof放在eclipse的MAT中打開。

Ok,下面將給大家介紹一個屌炸天的工具 -- LeakCanary 。

使用 LeakCanary 檢測 Android 的內(nèi)存泄漏

什么是LeakCanary呢?為什么選擇它來檢測 Android 的內(nèi)存泄漏呢?

別急,讓我來慢慢告訴大家!

LeakCanary 是國外一位大神 Pierre-Yves Ricau 開發(fā)的一個用于檢測內(nèi)存泄露的開源類庫。一般情況下,在對戰(zhàn)內(nèi)存泄露中,我們都會經(jīng)過以下幾個關(guān)鍵步驟:

1、了解 OutOfMemoryError 情況。

2、重現(xiàn)問題。

3、在發(fā)生內(nèi)存泄露的時候,把內(nèi)存 Dump 出來。

4、在發(fā)生內(nèi)存泄露的時候,把內(nèi)存 Dump 出來。

5、計算這個對象到 GC roots 的最短強引用路徑。

6、確定引用路徑中的哪個引用是不該有的,然后修復(fù)問題。

很復(fù)雜對吧?

如果有一個類庫能在發(fā)生 OOM 之前把這些事情全部都搞定,然后你只要修復(fù)這些問題就好了。LeakCanary 做的就是這件事情。你可以在 debug 包中輕松檢測內(nèi)存泄露。

一起來看這個例子(摘自 LeakCanary 中文使用說明,下面會附上所有的參考文檔鏈接):

class?Cat?{

}class?Box?{

Cat?hiddenCat;

}class?Docker?{????//?靜態(tài)變量,將不會被回收,除非加載?Docker?類的?ClassLoader?被回收。

static?Box?container;

}//?...Box?box?=?new?Box();//?薛定諤之貓Cat?schrodingerCat?=?new?Cat();

box.hiddenCat?=?schrodingerCat;

Docker.container?=?box;

創(chuàng)建一個RefWatcher,監(jiān)控對象引用情況。

//?我們期待薛定諤之貓很快就會消失(或者不消失),我們監(jiān)控一下refWatcher.watch(schrodingerCat);

當發(fā)現(xiàn)有內(nèi)存泄露的時候,你會看到一個很漂亮的 leak trace 報告:

GC ROOT static Docker.container

references Box.hiddenCat

leaks Cat instance

我們知道,你很忙,每天都有一大堆需求。所以我們把這個事情弄得很簡單,你只需要添加一行代碼就行了。然后 LeakCanary 就會自動偵測 activity 的內(nèi)存泄露了。

public?class?ExampleApplication?extends?Application?{

@Override

public?void?onCreate()?{

super.onCreate();

LeakCanary.install(this);

}

}

然后你會在通知欄看到這樣很漂亮的一個界面:

以很直白的方式將內(nèi)存泄露展現(xiàn)在我們的面前。

Demo

一個非常簡單的 LeakCanary demo:一個非常簡單的 LeakCanary demo: https://github.com/liaohuqiu/leakcanary-demo

接入

在 build.gradle 中加入引用,不同的編譯使用不同的引用:

dependencies?{

debugCompile?'com.squareup.leakcanary:leakcanary-android:1.3'

releaseCompile?'com.squareup.leakcanary:leakcanary-android-no-op:1.3'

}

如何使用

使用 RefWatcher 監(jiān)控那些本該被回收的對象。

RefWatcher?refWatcher?=?{...};

//?監(jiān)控

refWatcher.watch(schrodingerCat);

LeakCanary.install() 會返回一個預(yù)定義的 RefWatcher,同時也會啟用一個 ActivityRefWatcher,用于自動監(jiān)控調(diào)用 Activity.onDestroy() 之后泄露的 activity。

在Application中進行配置 :

public?class?ExampleApplication?extends?Application?{

public?static?RefWatcher?getRefWatcher(Context?context)?{

ExampleApplication?application?=?(ExampleApplication)?context.getApplicationContext();

return?application.refWatcher;

}

private?RefWatcher?refWatcher;

@Override

public?void?onCreate()?{

super.onCreate();

refWatcher?=?LeakCanary.install(this);

}

}

使用 RefWatcher 監(jiān)控 Fragment:

public?abstract?class?BaseFragment?extends?Fragment?{

@Override

public?void?onDestroy()?{

super.onDestroy();

RefWatcher?refWatcher?=?ExampleApplication.getRefWatcher(getActivity());

refWatcher.watch(this);

}

}

使用 RefWatcher 監(jiān)控 Activity:

public?class?MainActivity?extends?AppCompatActivity?{

......

@Override

protected?void?onCreate(Bundle?savedInstanceState)?{

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);????????????//在自己的應(yīng)用初始Activity中加入如下兩行代碼

RefWatcher?refWatcher?=?ExampleApplication.getRefWatcher(this);

refWatcher.watch(this);

textView?=?(TextView)?findViewById(R.id.tv);

textView.setOnClickListener(new?View.OnClickListener()?{

@Override

public?void?onClick(View?v)?{

startAsyncTask();

}

});

}

private?void?async()?{

startAsyncTask();

}

private?void?startAsyncTask()?{

//?This?async?task?is?an?anonymous?class?and?therefore?has?a?hidden?reference?to?the?outer

//?class?MainActivity.?If?the?activity?gets?destroyed?before?the?task?finishes?(e.g.?rotation),

//?the?activity?instance?will?leak.

new?AsyncTask()?{

@Override

protected?Void?doInBackground(Void...?params)?{

//?Do?some?slow?work?in?background

SystemClock.sleep(20000);

return?null;

}

}.execute();

}

}

工作機制

1.RefWatcher.watch() 創(chuàng)建一個 KeyedWeakReference 到要被監(jiān)控的對象。

2.然后在后臺線程檢查引用是否被清除,如果沒有,調(diào)用GC。

3.如果引用還是未被清除,把 heap 內(nèi)存 dump 到 APP 對應(yīng)的文件系統(tǒng)中的一個 .hprof 文件中。

4.在另外一個進程中的 HeapAnalyzerService 有一個 HeapAnalyzer 使用HAHA 解析這個文件。

5.得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位內(nèi)存泄露。

6.HeapAnalyzer 計算 到 GC roots 的最短強引用路徑,并確定是否是泄露。如果是的話,建立導(dǎo)致泄露的引用鏈。

7.引用鏈傳遞到 APP 進程中的 DisplayLeakService, 并以通知的形式展示出來。

ok,這里就不再深入了,想要了解更多就到作者 github 主頁這去哈。

總結(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,游標 Cursor,Stream,Bitmap等資源的使用,應(yīng)該在Activity銷毀時及時關(guān)閉或者注銷。

保持對對象生命周期的敏感,特別注意單例、靜態(tài)對象、全局性集合等的生命周期。

以上部分圖片、實例代碼和文段都摘自或參考以下文章 :

支付寶:

Android怎樣coding避免內(nèi)存泄露

支付寶錢包Android內(nèi)存治理

IBM :

Java的內(nèi)存泄漏

Android Design Patterns :

How to Leak a Context: Handlers & Inner Classes

伯樂在線團隊:

Android性能優(yōu)化之常見的內(nèi)存泄漏

我廠同學 :

Dalvik虛擬機 Finalize 方法執(zhí)行分析

騰訊bugly :

內(nèi)存泄露從入門到精通三部曲之基礎(chǔ)知識篇

內(nèi)存泄露從入門到精通三部曲之排查方法篇

內(nèi)存泄露從入門到精通三部曲之常見原因與用戶實踐

LeakCanary :

LeakCanary 中文使用說明

LeakCanary: 讓內(nèi)存泄露無所遁形

https://github.com/square/leakcanary

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏...
    _痞子閱讀 1,695評論 0 8
  • 被文同時發(fā)布在CSDN上,歡迎查看。 APP內(nèi)存的使用,是評價一款應(yīng)用性能高低的一個重要指標。雖然現(xiàn)在智能手機的內(nèi)...
    大圣代閱讀 4,956評論 2 54
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏...
    apkcore閱讀 1,303評論 2 7
  • 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏大家都不陌生了,簡單粗俗的講,...
    DreamFish閱讀 863評論 0 5
  • 這個題目是剛剛一瞬間想到的,想到堅持每天寫點東西,就當湊數(shù)吧。職業(yè)有多種,角色有很多,而你想成為一個什么樣的...
    杏林園中一顆草閱讀 346評論 0 0

友情鏈接更多精彩內(nèi)容