Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android應用程序內存洩漏介紹

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行的示意圖。
此處輸入圖片的描述vcq9vfjQ0MTatOa53MDto6y/ydLUz/uz/dL908PRrbu3tcTOyszio6zA/cjn09DI/bj2ttTP86Osz+C7pdL908OjrNa70qrL/MPHus24+b34s8yyu7/JtO+1xKOsxMfDtEdD0rLKx7/J0tS72MrVy/zDx7XEoaPV4tbWt73KvbXE08W148rHudzA7cTatOa1xL6rtsi63Ljfo6y1q8rH0KfCyr3Ptc2ho8HtzeLSu9bWs6PTw7XExNq05rncwO28vMr1ysfKudPDvMbK/cb3o6zA/cjnQ09NxKPQzbLJ08O8xsr9xve3vcq9udzA7bm5vP6jrMv80+vT0M/yzbzP4LHIo6y+q7bI0NC1zSi63MTRtKbA7dGtu7fS/dPDtcTOysziKaOstavWtNDQ0KfCyrrcuN+hozxiciAvPg0KPHN0cm9uZz5KYXZh1tC1xMTatObQucKpPC9zdHJvbmc+PGJyIC8+DQrPwsPmo6zO0sPHvs2/ydLUw+jK9sqyw7TKx8TatObQucKpoaPU2kphdmHW0KOsxNq05tC5wqm+zcrHtObU2tK70Kmxu7fWxeS1xLbUz/OjrNXi0Km21M/z09DPwsPmwb249szYteOjrMrXz8ijrNXi0Km21M/zyse/ybTvtcSjrLy01NrT0M/yzbzW0KOstObU2s2owre/ydLU0+vG5M/gwayju8bktM6jrNXi0Km21M/zysfO3tPDtcSjrLy0s8zQ8tLUuvOyu7vh1NnKudPD1eLQqbbUz/Oho8jnufu21M/zwvrX49Xiwb249sz1vP6jrNXi0Km21M/zvs2/ydLUxdC2qM6qSmF2YdbQtcTE2rTm0LnCqaOs1eLQqbbUz/Oyu7vhsbtHQ8v5u9jK1aOsyLu2+Mv8yLTVvNPDxNq05qGjPGJyIC8+DQrU2kMrK9bQo6zE2rTm0LnCqbXEt7bOp7j8tPPSu9CpoaPT0NCpttTP87G7t9bF5MHLxNq05r/VvOSjrMi7uvPItLK7v8m076Os08nT2kMrK9bQw7vT0EdDo6zV4tCpxNq05r2r08DUtsrVsru72MC0oaPU2kphdmHW0KOs1eLQqbK7v8m077XEttTP87a808lHQ7i61PC72MrVo6zS8rTLs8zQ8tSxsrvQ6NKqv7zCx9Xisr+31rXExNq05tC5wrahozxiciAvPg0Kzai5/bfWzvajrM7Sw8e1w9aqo6y21NPaQysro6yzzNDy1LHQ6NKq19S8urncwO2x37rNtqW146Ostvi21NPaSmF2YbPM0PLUsda70OjSqrncwO2x377Nv8nS1MHLKLK70OjSqrncwO22pbXjtcTKzbfFKaGjzai5/dXi1ta3vcq9o6xKYXZhzOG438HLseCzzLXE0KfCyqGjPC9wPg0KPHA+PGltZyBhbHQ9"此處輸入圖片的描述" src="/uploadfile/Collfiles/20160405/20160405093405202.gif" title="\" />
因此,通過以上分析,我們知道在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 = 1; i < 100; 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,無法釋放內存。
這裡寫圖片描述

或者在DDMS中也可以觀察
此處輸入圖片的描述

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,分析懷疑洩露的類:
此處輸入圖片的描述
B)分析持有此類對象引用的外部對象
此處輸入圖片的描述
C)分析這些持有引用的對象的GC路徑
此處輸入圖片的描述
D)逐個分析每個對象的GC路徑是否正常
此處輸入圖片的描述
從這個路徑可以看出是一個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 .
此處輸入圖片的描述seopoaM8YnIgLz4NCjxpbWcgYWx0PQ=="此處輸入圖片的描述" src="/uploadfile/Collfiles/20160405/20160405093405210.png" title="\" />
F)分析對比結果
此處輸入圖片的描述
可以看出兩個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);
  }
}

然後你會在通知欄看到這樣很漂亮的一個界面:
此處輸入圖片的描述

以很直白的方式將內存洩露展現在我們的面前。
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() {
        @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 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分析工具,來分析上述代碼:
第一步:
運行上述代碼,橫豎屏多次後,點擊下圖。
這裡寫圖片描述

經常上述操作後,會生成
這裡寫圖片描述

這裡要把生成的hprof文件轉換成標准的hprof文件,然後用MAT打開即可。
這裡寫圖片描述

然後在用MAT打開
這裡寫圖片描述

點擊Histogram我們可以看到輸入我們懷疑的洩漏對象,“activity”
可以看到我們的MainActivity中有兩個實例,懷疑這兩個中有一個已經洩漏了,繼續往下面分析,點擊右鍵選擇list incoming object
可以引用這個兩個Activity的信息。
這裡寫圖片描述

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

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved