應用記憶體洩露起因與解決方案分析
java gc機制
java記憶體管理與c/c++不同,java使用garbage collection
機制,由虛擬機器管理記憶體。在大部分虛擬機器(包括android的ART)中,都採用了“可達性”分析演算法來進行記憶體管理。
原理是:選取某幾個root節點,從root開始層層遍歷,如果找不到對該物件的引用鏈,則該物件被標記為不可達,等待gc回收。
記憶體洩漏的起因
如果引用鏈中長期存在著對該物件的引用(強引用),則該物件一直不能被gc銷燬。一兩次並沒有多大影響,如果頻繁發生,則可用記憶體會逐漸不足,在某次申請記憶體時會發生OOM,導致程式崩潰。
這裡強調是強引用,下面簡單介紹下java中的幾種引用型別:
- SoftReference : 實現某些對記憶體敏感的快取,記憶體足夠時就會保留不被回收,記憶體不夠時被gc銷燬。
- WeakReference:實現一些規範化的對映關係,當key或value沒有被引用時可以自動回收,該引用型別不會對該物件在gc中的回收產生任何影響,因此如果想持有一個物件的引用,但是又不想幹涉它的生命週期,使用WeakReference。
- PlantomReference:實現了預驗清理的工作,同樣不會干涉物件的生命週期,但是它的get方法總是返回null,不能獲得引用的物件,其儲存了
ReferenceQueue
的軌跡,允許獲知物件何時從記憶體中清除。
所以如果需要使用在可能發生記憶體洩漏的環境中引用Activity
Bitmap
等,使用WeakReference
最好。
洩露常見例子
由上文可知,洩露是由於長期持有物件的強引用導致,所以我們可以用以下方法強行製造洩露。
static
用static
修飾的變數或者引用,它們的所屬的是類,生命週期與類的生命週期一致,貫穿App的啟動到關閉。所以如果用static
修飾一個大物件的引用,就會產生洩露。例如:
static Activity activity;
static View view;
Activity是四大元件之一,其物件佔有的記憶體較大,用static
修飾容易發生洩露。
View雖然佔有的記憶體並不大,但是其建構函式中需要傳入Context
Activity
,也就間接持有了Activity
的引用,容易洩露。
innerClass
非靜態內部類會持有外部類的引用。具體見這篇文章。大致上就是說在編譯階段,編譯器會給非靜態加上一個成員變數,其型別與外部類型別相同,在構造方法的引數上新增上外部類的一個引用變數,並在初始化內部類的時候傳入外部類的例項。如下:
//以下並不符合java程式碼規範,僅演示在編譯階段新增的程式碼
public class Test(){
public static void mian(String[] args){
new InnerTest(this); // 將當前例項引用傳給內部類建構函式的第二個引數
}
class InnerTest{
Test test; // 新增一個引用變數
InnerTest(InnerTest this, Test test){ // 新增一個當前類的this變數和外部類的引用變數
this.test = test; // 對引用變數賦值
}
}
}
一旦非靜態內部類例項長期存活,由於強引用的關係,外部類例項也長期存活,這樣就造成了可能的記憶體洩漏。
例如AsyncTask
處理後臺任務,我們很多時候採用匿名內部類去建立:
void leakAsyncTask(){
new AsyncTask<Void, Void, Void>(){
@Override
protected Void doInBackground(Void... voids) {
while (true){
....
}
return null;
}
}
}
在AS中寫出上述程式碼,AS會提醒我們需要使用靜態內部類否則會出現記憶體洩漏,無獨有偶,當我們使用Thread
和Handler
的時候同樣會提醒我們去建立靜態內部類。
register
當我們使用第三方庫的時候,比如ButterKnife等等,有時候需要獲取系統服務,我們在建立物件的時候會將Activity
例項的引用傳過去進行register
或者bind
,而它們在執行的時候我們不易察覺,如果我們沒有在Activity
被銷燬之前取消訂閱或者繫結,由這些後臺程式持有著我們Activity
的引用,就會造成記憶體洩漏。
洩漏檢測
準備工作
AS中的Android Profiler是專門用來檢測應用的執行情況:
左上角的垃圾箱形狀的按鈕是用來強制進行一次gc,第二個按鈕是Dump java heap
,這個按鈕可以產生一個當前java堆的.hprof檔案,用來記錄當前時刻java堆中的記憶體情況。
而圖中各種顏色的含義都在圖上標明瞭,可以看到藍色的變化很大,並且過段時間就會驟降,很顯然這代表了java的堆記憶體,可以很清晰地看到各個時刻的記憶體情況。
肉眼觀察
下面看看真實的記憶體洩露是什麼樣子,我在一個Activity
中啟動了另外一個Activity
,在後者中增加一個static
變數mContext
,並將該Activity
例項引用賦值給該變數,顯然按照上面的說法是會出現洩露的,我們看看結果:
圖中的上升沿是我點選啟動另一個Activity
時的記憶體變化,由於static
變數一直持有著Activity
的引用,所以gc時無法銷燬,可以看到與上面圖的區別就是隻有上升沿沒有下降沿。長時間肯定會出現記憶體崩潰。
自動分析
在上面頻繁啟動Activity
後點擊Dump java heap
按鈕,過一會我們可以看到AS為我們Dump出此時的記憶體記錄。
找到所在包剛剛定義的類,如下:
可以明顯看到本來應該大小相似的兩個類,由於持有了static
型別的變數,其佔有的記憶體大小是第一個Activity
的好幾倍,如果頻繁這樣點選,最終肯定會造成記憶體洩漏:
還可以匯出Dump的檔案然後用MAT進行分析,但是最簡便的方法當然是:
使用Square公司的Leakcanary進行分析,此處是地址。