Android應用程式記憶體流失介紹

來源:互聯網
上載者:User

標籤:

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行的。

Java使用有向圖的方式進行記憶體管理,可以消除引用迴圈的問題,例如有三個對象,相互引用,只要它們和根進程不可達的,那麼GC也是可以回收它們的。這種方式的優點是管理記憶體的精度很高,但是效率較低。另外一種常用的記憶體管理技術是使用計數器,例如COM模型採用計數器方式管理構件,它與有向圖相比,精度行低(很難處理循環參考的問題),但執行效率很高。
Java中的記憶體流失
下面,我們就可以描述什麼是記憶體流失。在Java中,記憶體流失就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;其次,這些對象是無用的,即程式以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的記憶體流失,這些對象不會被GC所回收,然而它卻佔用記憶體。
在C++中,記憶體流失的範圍更大一些。有些對象被分配了記憶體空間,然後卻不可達,由於C++中沒有GC,這些記憶體將永遠收不回來。在Java中,這些不可達的對象都由GC負責回收,因此程式員不需要考慮這部分的記憶體泄露。
通過分析,我們得知,對於C++,程式員需要自己管理邊和頂點,而對於Java程式員只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式,Java提高了編程的效率。


因此,通過以上分析,我們知道在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 .

D)開啟第二個HPROF 檔案然後重做步驟2和3.
E)切換到Compare Basket view, 然後點擊Compare the Results (視圖右上方的紅色”!”表徵圖)。

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 {

......@Overrideprotected 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 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被泄漏。

參考文檔:
IBM:Java記憶體流失
LeakCanery:LeakCanery中文使用手冊
MAT:MAT使用教程

Android應用程式記憶體流失介紹

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.