1. 程式人生 > >應用記憶體洩露起因與解決方案分析

應用記憶體洩露起因與解決方案分析

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會提醒我們需要使用靜態內部類否則會出現記憶體洩漏,無獨有偶,當我們使用ThreadHandler的時候同樣會提醒我們去建立靜態內部類。

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的好幾倍,如果頻繁這樣點選,最終肯定會造成記憶體洩漏:

第二個Activity中擁有6個例項變數

還可以匯出Dump的檔案然後用MAT進行分析,但是最簡便的方法當然是:

使用Square公司的Leakcanary進行分析,此處是地址