1. 程式人生 > >Android 記憶體分析指北

Android 記憶體分析指北

android 記憶體洩漏分析指北

簡單來說記憶體洩漏就是當物件不再被應用程式使用,但是垃圾回收器卻不能移除它們,因為它們正在被引用

java 垃圾回收介紹:

Java 虛擬機器執行所管理的記憶體包括以下幾個執行時的資料區域
如下圖:

程式計數器: 一塊比較小的記憶體區域,可以看作是當前執行緒所執行的位元組碼的行號指示器。且每個執行緒都有一個獨立的程式計數器。

java 虛擬機器棧: 執行緒私有的,描述的是java 方法執行的記憶體模型,每個執行緒執行的時候都會建立一個棧幀用於儲存 區域性變數、運算元棧、動態連結、方法出口、等資訊。一個方法的呼叫到執行結束的過程就對應一個棧幀在虛擬機器棧中的入棧到處棧的過程。 虛擬機器區域性變量表中存放了編譯可知的各種區域性資料型別(boolean、byte、char、short、int 、float 、long、double)

、物件引用、返回地址 。

本地方法棧:和虛擬機器棧類似,其中虛擬機器棧為執行java 方法服務,而本地方法棧為虛擬機器使用的native 的方法服務java 堆:java 虛擬機器所管理的記憶體中最大的一塊,且其是被所有的執行緒共享的一塊記憶體區域,在虛擬機器啟動的時候建立。該區域的唯一目的就是來存放物件例項的。 java 堆是垃圾啊回收管理的主要區域,因此在很多的時候被叫做"GC堆"

方法區:和java 堆一樣是各個執行緒共享的記憶體區域,用來儲存已經被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼和資料。
執行中常量池:為方法區的一部分。class 檔案中除了有類的版本、欄位、方法、介面等描述資訊外、還有一項資訊是常量池、用來存放編譯期生產的各種字面量和符號引用。

GC 時那些記憶體需要釋放:

首先對於程式計數器、虛擬機器棧、本地方法棧 這3個區域都是隨執行緒而生、所執行緒而亡。 棧中的棧幀隨著方法的進入和退出執行著如棧和出棧的操作。每一個棧幀中分配的記憶體基本上在類結構確定下來的時候已經是可知的了。所以在這幾個區域就不需要考慮記憶體回收的問題,因為在方法結束,或者執行緒結束的時候記憶體自然就會回收了。
但是java 堆和方法區確不一樣,如一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的的多個分支需要的記憶體也可能不一樣,我們只有在程式處於執行的情況下才可以知道會建立那些物件,這部分的記憶體的分配和回收也是動態的,垃圾回收所關注的也這一部分的內容。

確定哪些物件是存活的,那些已經“死去”(即不可能被任何途徑使用的物件)
方式:
A、引用計數法: 給物件新增一個引用計數器,每當有一個地方引用它時,計數器就加1 ; 當引用失效時就減1;任何時刻計數器為0 的物件是就不可能再被使用的。 (主流的JAVA 虛擬器並沒有使用引用計數法來管理記憶體,其中主要的原因是它很難解決物件的相互迴圈引用的問題) 。

B、可達性分析法: 通過一系列的稱為GC Roots 的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Rotts 沒有任何引用鏈 相連時(即GC Roots 到這個物件不可達),則證明這個物件是不可用的。
如下圖論:

如上圖所示,object1~object4對GC Root都是可達的,說明不可被回收,object5和object6對GC Root節點不可達,說明其可以被回收

可作為GC roots 的物件包括:虛擬機器棧(棧幀中的本地變量表)中引用的物件(就是指正執行的方法中的區域性變數,引數,臨時值所引用的物件)、方法區中的靜態屬性引用的物件、方法區中常量引用的物件、本地方法中JNI 引用的物件。

各中引用:
強引用:只要強引用還存在,垃圾收集器將永遠不會回收掉被引用的物件。

軟引用:用來描述一些還有用但並非必須的物件、在系統將要發生記憶體溢位異常之前,將會把這些物件列進物件回收範圍中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位的異常。

弱引用:也是用來描述非必須物件,其強度比軟引用更弱些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前,當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。

虛引用:一種最弱的引用關係、一個物件是否有引用存在完全不會影響其生存時間、且無法通過虛引用活得一個物件的例項。一個虛引用唯一的目的就是在這個物件在被收集器回收的時候能夠收到一個系統通知。

參考:深入理解java 虛擬機器第二版

記憶體洩漏檢測:

1、Dump Appmeminfo:在一個App 中存在著很多個Activity ,其實我們只需要一個一個介面 去檢查其是否存在Activity洩漏的情況 ,
利用adb shell dumpsys meminfo <package name>dumpapp 的記憶體資訊,該資訊中有包含當前 app 中所未釋放的 activity 的數量,以及view 的個數,如下圖所示, 在進入一個介面之前檢查下activity 數量和view 的數量 ,在退出該介面後檢查下在檢視一邊activityview 的數量,對比進入和退出後activity 和view 的數量是否有差異。
如下圖:目前存在著1 個activity 184 個 view 。 因此我們如果要檢查某個activity 是否存在洩漏,我們只需要在進入該activity 之前dump 下該資訊, 然後在進入該activity ,進行一系列操作,然後點選back鍵,退出 。

ps:dump meminfo 資訊的時候,需要等記憶體穩定下然後進行dump, 或者通過手動gc(利用 androidmonitor 工具手動gc 按鈕)後進行dump 操作。

2、跑自動化測試指令碼,或者跑Monkey 檢視記憶體的增長曲線(測試檢測的方式)。

3、LeakCanary Square 公司開源作品,使用方便,可以直接定位到洩漏的物件,並且給出呼叫鏈。

記憶體洩漏分析,相關工具使用:

分析記憶體洩漏,第一步得復現問題,然後抓取hprof 檔案。

1、androidStudio Hprof 分析Activity 洩漏:利用Android studioMonitors 的工具抓取 點選Start Allocation Tracking 抓取hprof 檔案。 具體怎樣使用該工具 可參考如下連結。
hporf 分析

例如如下分析結果,存在 HandleActivity 存在洩漏。 然後直接參考上面的連結,找出對應引起洩漏的點。 下圖是HandlerActivity的內部類釋放不了造成Activity 洩漏的。

2、 MAT 使用:MAT工具開啟hprof 檔案時,需要先利用hprof-conv 工具將hprof 檔案轉換下, 利用如下命令。

hprof-conv demo.hprof demo_conv.hprof

開啟hprof檔案如下:搜尋關鍵字可以搜尋出相關物件物件的個數,所佔的記憶體(如下圖)。

右鍵Merge shortest paths to gc roots 選擇 exclude all phantom/weak/soft etc references 可以定位到gc root 這樣就可以確定時哪個物件沒有釋放導致HandlerActiivty 不能釋放 。

對比兩個hprof 檔案,檢視某個物件的個數對比如下圖:

通過上圖就可以看出HandlerActivity 沒有被釋放。

常見記憶體洩漏型別以及解決方案:

靜態引用造成的記憶體洩漏
private static DeviceUtil stance;![Alt text](./mat_2.png)

private Context context;
....
public static DeviceUtil getStance(Context context) {
    if(stance == null){
        synchronized (DeviceUtil.class){
            stance = new DeviceUtil(context);
        }
    }   
    return stance;
}

private DeviceUtil(Context context){
    this.context = context;
}

呼叫的地方:

private void showVersionCode(){
    final int versionCode = DeviceUtil.getStance(HandlerActivity.this)
            .getVersionCode(getPackageName());
    Toast.makeText(HandlerActivity.this,
            "versionCode="+versionCode,Toast.LENGTH_LONG).show();
}

在上面的例子中DeviceUtil 持有了Activity從而導致其釋放不了。

解決方案:Activity context 修改成Application context . 因為Applicaition 時全域性的,生命週期時和app 生命週期一樣的。

非靜態內部類造成記憶體洩漏:
public class HandlerActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_handler);
    startThread();
}

class InnerThread extends Thread{
    @Override
    public void run() {
        super.run();
        int index = 0;
        while (index < Integer.MAX_VALUE){
            index ++;
            try {
                sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

private void startThread(){
    InnerThread thread = new InnerThread();
    thread.start();
}

如上面程式碼中InnerThread 會隱式 的持有外部類的引用,因為在這裡InnerThread 執行緒的生命週期超過了Activity 的生命週期,當finish 當前ActivityInnerThread 並不會停止且持有了當前Activity 從而導致HandleActivity 洩漏。
解決方案: 將內部類修改為靜態內部類的方式 。 如下:

static class InnerThread extends Thread{
    @Override
    public void run() {
        super.run();
        int index = 0;
        while (index < Integer.MAX_VALUE){
            index ++;
            try {
                sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } 
        }
    }
}
匿名內部類造成的記憶體洩漏
public class HandlerActivity extends AppCompatActivity {



@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_handler);
    //startThread();
    Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            final int what = msg.what;
            if(what == 0){ 
                Toast.makeText(HandlerActivity.this,"receive message",Toast.LENGTH_LONG).show();
            }
            super.handleMessage(msg);
        }
    };

    Message message = handler.obtainMessage(0);
    handler.sendMessageDelayed(message,1000*10);
}

在這裡Message 會在主執行緒中存在10s , 且Message 會持有handler 物件,而handler 會隱式持有Activity ,從而導致Activity 洩漏。
修改方案: 將匿名內部類修改成靜態內部類即可,參考上面的例子。

不需要的監聽未移除導致的記憶體溢位:

常見的如:廣播監聽沒有被移除,所以需要註冊和解除註冊成對出現 。

Native 洩漏:

在android 中Native 洩漏都大部分是通過jni 呼叫Native 方法,所以只需要檢查Java 端呼叫JNI 的地方即可。

binder 洩漏:
資源物件未關閉 :

資源性物件如Cursor、File、Socket,應該在使用後及時關閉。未在finally中關閉,會導致異常情況下資源物件未被釋放的隱患。

總結:一般來說,記憶體洩漏都是因為洩漏物件的引用被傳遞到該物件的範圍之外,或者說記憶體洩漏是因為持有物件的長期引用,導致物件無法被 GC 回收。為了避免這種情況,我們可以選擇在物件生命週期結束的時候,解除繫結,將引用置為空,或者使用弱引用。