天天看點

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

Android應用程式記憶體洩漏介紹

記憶體洩漏和記憶體溢出的差別

記憶體溢出(out of memory)是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory。比如在我們每個Android程式在運作時系統都會給程式配置設定一個一定的記憶體空間,當程式在運作中需要的記憶體超出這個限制就會報記憶體溢出(out of memory)。

記憶體洩漏(memory leak)是指程式在申請記憶體後,無法釋放已申請的記憶體空間。多次記憶體無法被釋放,程式占用的記憶體會一直增加,直到超過系統的記憶體限制報記憶體溢出。

java中為什麼會發生記憶體洩漏

大家在學習Java的時候,可以在閱讀相關書籍的時候,關于Java的優點中,第一條就是Java是通過GC來自動管理記憶體的回收的,程式員不需要通過調用函數來釋放記憶體。是以,很多人認為Java不存在記憶體洩漏的問題,真實的情況并不是這樣,尤其是我們在開發手機和平闆相關的應用的時候,往往是由于記憶體洩漏的累計很快導緻程式的崩潰。想要了解這個問題,我們需要先了解Java是如何管理記憶體。

Java的記憶體管理

Java的記憶體管理就是對象的配置設定和釋放的問題,在Java中,程式員需要需要通過關鍵字new為每個對象申請記憶體空間(基本類型除外),所有的對象都在堆(Heap)中配置設定空間。另外,對象的釋放是由GC決定和執行的。在Java中,記憶體的配置設定是由程式完成的,而記憶體的釋放由GC完成的。這種收支兩條線的确是簡化了程式員的工作。但同時,它也加重了JVM的負擔。這也是Java運作較慢的原因之一。因為,GC為了正确的釋放每個對象,GC必須監控每個對象的運作狀态,包括對象的申請,引用,被引用,指派GC都需要監控。

監視對象狀态是為了更加準确地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。

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

以下,我們舉一個例子說明如何用有向圖表示記憶體管理。對于程式的每一個時刻,我們都有一個有向圖表示JVM的記憶體配置設定情況。以下右圖,就是左邊程式運作到第6行的示意圖。

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

Java使用有向圖的方式進行記憶體管理,可以消除引用循環的問題,例如有三個對象,互相引用,隻要它們和根程序不可達的,那麼GC也是可以回收它們的。這種方式的優點是管理記憶體的精度很高,但是效率較低。另外一種常用的記憶體管理技術是使用計數器,例如COM模型采用計數器方式管理構件,它與有向圖相比,精度行低(很難處理循環引用的問題),但執行效率很高。

Java中的記憶體洩漏

下面,我們就可以描述什麼是記憶體洩漏。在Java中,記憶體洩漏就是存在一些被配置設定的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;其次,這些對象是無用的,即程式以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的記憶體洩漏,這些對象不會被GC所回收,然而它卻占用記憶體。

在C++中,記憶體洩漏的範圍更大一些。有些對象被配置設定了記憶體空間,然後卻不可達,由于C++中沒有GC,這些記憶體将永遠收不回來。在Java中,這些不可達的對象都由GC負責回收,是以程式員不需要考慮這部分的記憶體洩露。

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

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

是以,通過以上分析,我們知道在Java中也有記憶體洩漏,但範圍比C++要小一些。因為Java從語言上保證,任何對象都是可達的,所有的不可達對象都由GC管理。

對于程式員來說,GC基本是透明的,不可見的。雖然,我們隻有幾個函數可以通路GC,例如運作GC的函數System.gc(),但是根據Java語言規範定義, 該函數不保證JVM的垃圾收集器一定會執行。因為,不同的JVM實作者可能使用不同的算法管理GC。通常,GC的線程的優先級别較低。JVM調用GC的政策也有很多種,有的是記憶體使用到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。除非在一些特定的場合,GC的執行影響應用程式的性能,例如對于基于Web的實時系統,如網絡遊戲等,使用者不希望GC突然中斷應用程式執行而進行垃圾回收,那麼我們需要調整GC的參數,讓GC能夠通過平緩的方式釋放記憶體,例如将垃圾回收分解為一系列的小步驟執行,Sun提供的HotSpot JVM就支援這一特性。

Android記憶體洩漏總結

在我們開發Android程式的時候,經常會遇到記憶體溢出的情況,在我們這次Launcher的開發過程中,就存在記憶體洩漏的問題。下面結合我們在Launcher開發中遇到的實際問題,分享一下記憶體洩漏怎麼解決。

Android中常見記憶體洩漏

  • 集合類洩漏
    集合類如果僅僅有添加元素的方法,而沒有相應的删除機制,導緻記憶體被占用。如果這個集合類是全局性的變量(比如類中的靜态屬性,全局性的 map 等即有靜态引用),那麼沒有相應的删除機制,很可能導緻集合所占用的記憶體隻增不減。請看下面的示例代碼,稍不注意還是很容易出現這種情況,比如我們都喜歡通過HashMap做一些緩存之類的事,這種情況就要多留一些心眼。
ArrayList list = new ArrayList();
    for (int i = ; i < ; i++) {
        Object o = new Object();
        v.add(o);
        o = null;   
    }
           

在上面的代碼中list是一個全局變量,僅僅在後面把對象的引用置空是沒有用的,因為對象被list持有,在本類生命周期沒有結束的情況下,是不會被gc回收的。

  • 單例造成的記憶體洩漏。
    由于單例的靜态特性使得其生命周期跟應用的生命周期一樣長,是以如果使用不恰當的話,使單例持有的對象一直存在,很容易造成記憶體洩漏。比如下面一個典型的例子:
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;
        }
        }
           

上面的例子中如果傳進來的Context是Activity,AppManager是靜态的變量,它的生命周期和Application是一樣的。由于該Activity一直被該該instance 一直持有,是以傳進來的Activity無法被回收。将會産生記憶體洩漏。解決方法:此處可以傳入Application,因為Application的生命周期是從開始到結束的。

  • 非靜态内部類建立靜态執行個體造成的記憶體洩漏
非靜态内部類預設持有該類,如果在本類中它的執行個體是靜态的,就表示它的生命周期是和Application一樣長。那麼預設非靜态内部類的靜态執行個體持有了該類,該資源不會被gc掉,導緻記憶體洩漏。
public class MainActivity extends Activity{

     private static LeakInstance mLeakInstance;
     @override
     public void onCreate(Bundle onsaveInstance){
        ......
     }

     class LeakInstance{
         .....
     }
}
           

上面的代碼片段中,LeakInstance 是一個在Activity中的内部類,它有一個靜态執行個體mLeakInstance,該靜态執行個體的生命周期和Application是一樣的,同時它預設持有了MainActivity,這樣會導緻Activity不會被gc掉,導緻記憶體洩漏。

匿名内部類運作在異步線程。

匿名内部類預設持有它所在類的引用,如果把這個匿名内部類放到一個線程中取運作,而這個線程的生命周期和這個類的生命周期不一樣的時候,會導緻該類被線程所持有,不能釋放。導緻記憶體洩漏。請看如下示例代碼:
public class MainActiviy extends Activity{

   private Therad mThread = null;

   private Runnable myRunnable = new Runnable{
        public void run{
           ......

        }
   }
       protected void onCreate(Bundle onSaveInstance){
               .......
              mThread = new Thread(myRunnable);
              mThread.start();
       }
}
           

在上面的例子中myRunnable 持有了MainActiviy,mThread的生命周期和Activity不一樣,MainActiviy會被持有直到Thread運作結束。導緻記憶體洩漏。

  • Handler 造成的記憶體洩漏

Handler 的使用造成的記憶體洩漏問題應該說是最為常見了,很多時候我們為了避免 ANR 而不在主線程進行耗時操作,在處理網絡任務或者封裝一些請求回調等api都借助Handler來處理,但 Handler 不是萬能的,對于 Handler 的使用代碼編寫一不規範即有可能造成記憶體洩漏。另外,我們知道 Handler、Message 和 MessageQueue 都是互相關聯在一起的,萬一 Handler 發送的 Message 尚未被處理,則該 Message 及發送它的 Handler 對象将被線程 MessageQueue 一直持有。

由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一緻的。是以這種實作方式一般很難保證跟 View 或者 Activity 的生命周期保持一緻,故很容易導緻無法正确釋放。可以看看下面的列子:

public MainActivity extends Activity{

    private Handler mHandler = new Handler();

    protected void onCreate(Bundle onSaveInstance){
        .......
        mHandler.postDelay(new Runnable(){


        },1000*1000);
    }
}
           

上述代碼中mHandler把delay很久,實際持有了MainActivity,如果在此Activity死掉,那麼他是無法被回收的。需要等待mHandlder釋放持有的資源。

如何發現記憶體洩漏

MAT

1.直接通過觀察Android Monitor的memory直覺的觀察,例如我們在開發Launcher的時候,Launcher的Activity在橫豎屏切換的時候就出現了記憶體洩漏的情況,這時候Memory的值會不斷的變大,且通過手動點選GC,無法釋放記憶體。

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

或者在DDMS中也可以觀察

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

2.通過MAT工具查找

Java 記憶體洩漏的分析工具有很多,但衆所周知的要數 MAT(Memory Analysis Tools) 和 YourKit 了。

MAT分析heap的總記憶體占用大小來初步判斷是否存在洩露

打開 DDMS 工具,在左邊 Devices 視圖頁面選中“Update Heap”圖示,然後在右邊切換到 Heap 視圖,點選 Heap 視圖中的“Cause GC”按鈕,到此為止需檢測的程序就可以被監視。

Heap視圖中部有一個Type叫做data object,即資料對象,也就是我們的程式中大量存在的類類型的對象。在data object一行中有一列是“Total Size”,其值就是目前程序中所有Java資料對象的記憶體總量,一般情況下,這個值的大小決定了是否會有記憶體洩漏。可以這樣判斷:

進入某應用,不斷的操作該應用,同時注意觀察data object的Total Size值,正常情況下Total Size值都會穩定在一個有限的範圍内,也就是說由于程式中的的代碼良好,沒有造成對象不被垃圾回收的情況。

是以說雖然我們不斷的操作會不斷的生成很多對象,而在虛拟機不斷的進行GC的過程中,這些對象都被回收了,記憶體占用量會會落到一個穩定的水準;反之如果代碼中存在沒有釋放對象引用的情況,則data object的Total Size值在每次GC後不會有明顯的回落。随着操作次數的增多Total Size的值會越來越大,直到到達一個上限後導緻程序被殺掉。

MAT分析hprof來定位記憶體洩露的原因所在

這是出現記憶體洩露後使用MAT進行問題定位的有效手段。

A)Dump出記憶體洩露當時的記憶體鏡像hprof,分析懷疑洩露的類:

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

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

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

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

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

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

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

從這個路徑可以看出是一個antiRadiationUtil工具類對象持有了MainActivity的引用導緻MainActivity無法釋放。此時就要進入代碼分析此時antiRadiationUtil的引用持有是否合理(如果antiRadiationUtil持有了MainActivity的context導緻節目退出後MainActivity無法銷毀,那一般都屬于記憶體洩露了)。

MAT對比操作前後的hprof來定位記憶體洩露的根因所在

為查找記憶體洩漏,通常需要兩個 Dump結果作對比,打開 Navigator History面闆,将兩個表的 Histogram結果都添加到 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 .

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

D)打開第二個HPROF 檔案然後重做步驟2和3.

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

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

F)分析對比結果

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

可以看出兩個hprof的資料對象對比結果。

通過這種方式可以快速定位到操作前後所持有的對象增量,進而進一步定位出目前操作導緻記憶體洩露的具體原因是洩露了什麼資料對象。

注意:

如果是用 MAT Eclipse 插件擷取的 Dump檔案,不需要經過轉換則可在MAT中打開,Adt會自動進行轉換。

而手機SDk Dump 出的檔案要經過轉換才能被 MAT識别,Android SDK提供了這個工具 hprof-conv (位于 sdk/tools下)

首先,要通過控制台進入到你的 android sdk tools 目錄下執行以下指令:

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

例如 hprof-conv input.hprof out.hprof

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

下面将給大家介紹一個屌炸天的工具 – LeakCanary 。

LeakCanary

什麼是LeakCanary 呢?為什麼選擇它來檢測 Android 的記憶體洩漏呢?

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

LeakCanary 是國外一位大神 Pierre-Yves Ricau 開發的一個用于檢測記憶體洩露的開源類庫。一般情況下,在對戰記憶體洩露中,我們都會經過以下幾個關鍵步驟:

1、了解 OutOfMemoryError 情況。

2、重制問題。

3、在發生記憶體洩露的時候,把記憶體 Dump 出來。

4、在發生記憶體洩露的時候,把記憶體 Dump 出來。

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

6、确定引用路徑中的哪個引用是不該有的,然後修複問題。

很複雜對吧?

如果有一個類庫能在發生 OOM 之前把這些事情全部都搞定,然後你隻要修複這些問題就好了。LeakCanary 做的就是這件事情。你可以在 debug 包中輕松檢測記憶體洩露。

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

class Cat{

}
class Box{
  Cat hiddenCat;
}

class Docker {
   //靜态變量,生命周期和Classload一樣。
   static Box cainter;
}
        // 薛定谔之貓
Cat schrodingerCat = new Cat();
box.hiddenCat = schrodingerCat;
Docker.container = box;
           
建立一個RefWatcher,監控對象引用情況。
// 我們期待薛定谔之貓很快就會消失(或者不消失),我們監控一下
refWatcher.watch(schrodingerCat);
           

當發現有記憶體洩露的時候,你會看到一個很漂亮的 leak trace 報告:

GC ROOT static Docker.container

references Box.hiddenCat

leaks Cat instance

我們知道,你很忙,每天都有一大堆需求。是以我們把這個事情弄得很簡單,你隻需要添加一行代碼就行了。然後 LeakCanary 就會自動偵測 activity 的記憶體洩露了。

public class ExampleApplication extends Application {
  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}
           

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

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

以很直白的方式将記憶體洩露展現在我們的面前。

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 監控那些本該被回收的對象。

RefWatcher refWatcher = {...};

// 監控
refWatcher.watch(schrodingerCat);
           

LeakCanary.install() 會傳回一個預定義的 RefWatcher,同時也會啟用一個 ActivityRefWatcher,用于自動監控調用 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 監控 Fragment:

public abstract class BaseFragment extends Fragment {

  @Override public void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
    refWatcher.watch(this);
  }
}
           

使用 RefWatcher 監控 Activity:

public class MainActivity extends AppCompatActivity {

......
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
        //在自己的應用初始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<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... params) {
            // Do some slow work in background
            SystemClock.sleep(20000);
            return null;
        }
    }.execute();
}
           

}

工作機制

1.RefWatcher.watch() 建立一個 KeyedWeakReference 到要被監控的對象。

2.然後在背景線程檢查引用是否被清除,如果沒有,調用GC。

3.如果引用還是未被清除,把 heap 記憶體 dump 到 APP 對應的檔案系統中的一個 .hprof 檔案中。

4.在另外一個程序中的 HeapAnalyzerService 有一個 HeapAnalyzer 使用HAHA 解析這個檔案。

5.得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位記憶體洩露。

6.HeapAnalyzer 計算 到 GC roots 的最短強引用路徑,并确定是否是洩露。如果是的話,建立導緻洩露的引用鍊。

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

ok,這裡就不再深入了,想要了解更多就到 作者 github 首頁。

Androidstudio自帶分析工具

使用Android Monitor中自帶的Memory工具,按照圖中所示,先點選GC,然後在生成hprof檔案。

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

然後打開輕按兩下生成的檔案

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

可以看到很快就查到了記憶體洩漏的原因。

一個記憶體溢出的列子

下面的示範一個記憶體洩漏的具體案例

在Android Studio中建立一個項目,建立一個APPManager的單例類:

public class AppManager {

    private static Context sContext;

    private static AppManager instance;

    public  static AppManager getInstance(Context context){
        if(instance==null){
            instance = new AppManager(context);
        }
        return instance;
    }

    private AppManager(Context context){
        sContext = context;
    }
}
           

在上述的代碼片段中,Context作為一個靜态的變量寫在類中。繼續看下面的代碼:

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });

        AppManager.getInstance(this);
    }
           

這個時候我們調用橫豎屏多次,之後發現

我們通常會在Activity中如上述執行個體代碼中那樣運用這個類。下面讓我們調用MAT分析工具,來分析上述代碼:

第一步:

運作上述代碼,橫豎屏多次後,點選下圖。

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

經常上述操作後,會生成

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

這裡要把生成的hprof檔案轉換成标準的hprof檔案,然後用MAT打開即可。

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

然後在用MAT打開

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

點選Histogram我們可以看到輸入我們懷疑的洩漏對象,“activity”

可以看到我們的MainActivity中有兩個執行個體,懷疑這兩個中有一個已經洩漏了,繼續往下面分析,點選右鍵選擇list incoming object

可以引用這個兩個Activity的資訊。

Android應用程式記憶體洩漏介紹Android應用程式記憶體洩漏介紹

已經很明确了,我們的一個Activity被sContext持有了,sContext是靜态的,它的生命周期是和Application的生命周期是一樣的,是以在整個Application的生命周期該Activity被洩漏。

參考文檔:

IBM:Java記憶體洩漏

LeakCanery:LeakCanery中文使用手冊

MAT:MAT使用教程