Android內存泄漏檢測工具使用手冊

前言

性能優化除過我們平時自己設計和開發之外就得考慮使用工具進行檢測。Android 關於能夠定位和剖析問題的內存工具有很多,但不是每個工具所有場景都能覆蓋到。

  • DDMS
  • LeakCanary
  • haha/shark
  • Android Profile
  • MAT
  • Jhat
  • dumpsys meminfo
  • APT
  • LeakInspector
  • Chrome Devtool
  • GC Log

現在對平時能發現問題,而且使用簡單的一些工具的使用進行整理,並且對這個 LeakCanaryTestActivity 頁面進行內存泄漏的分析。

public class LeakCanaryTestActivity extends BaseActivity {
    private static Test test;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        test = new Test(this);
    }
    private static class Test{
        public Test(Context context) {
            this.context = context;
        }
        private Context context;
        private int a;
        private int b;
    }
}

LeakCanary

LeakCanary 官網

LeakCanary 的原理很簡單: 在 ActivityFragment 被銷燬後, 將他們的引用包裝成一個 WeakReference, 然後將這個 WeakReference 關聯到一個 ReferenceQueue 。查看ReferenceQueue中是否含有 ActivityFragment 的引用。如果沒有 觸發GC 後再次查看。還是沒有的話就說明回收成功, 否則可能發生了泄露. 這時候開始 dump 內存的信息,並分析泄露的引用鏈。

在Android中接入LeakCanary

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'
}

LeakCanary2.0 之前我們接入的時候需要在 Application.onCreate 方法中顯式調用 LeakCanary.install(this); 開啓 LeakCanary 的內存監控。

LeakCanary2.0 開始通過自己註冊的 provider 自己開啓 LeakCanary 的內存監控。我們平時開發用的 Instant Run 運行過程中也使用的是這種靜默方式進行啓動。

<provider android:name="com.android.tools.ir.server.InstantRunContentProvider" 
    android:multiprocess="true" 
  android:authorities="com.tzx.androidcode.com.android.tools.ir.server.InstantRunContentProvider"/>

LeakCanary內存泄漏分析

在進行 debug 或者 UI自動化 測試的時候,我們會在通知欄看到有關內存泄漏的提示。查看詳情後我們能看到相關的內存泄漏具體位置,存在泄露的成員變量都用波浪線進行的標識。

LeakCanary-user

內存泄漏上報到服務端

LeakCanary 升級到 2.0betafinal 版本之後 shark 官網 文檔提供的的內存泄漏上報方式對應的 API 已經過時,我們需要實現新的接口將 LeakCanary 捕獲的內存泄漏進行上報。

class LeakUploader : OnHeapAnalyzedListener {
  override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
    TODO("Upload heap analysis to server")
    //HeapAnalysis的toString和2.0之前的版本的LeakCanary.leakInfo獲得的信息類似
    println(heapAnalysis)
  }
}
class MyApplication : Application() {
  override fun onCreate() {
    super.onCreate()
    LeakCanary.config = LeakCanary.config.copy(
        onHeapAnalyzedListener = LeakUploader()
    )
  }
}

Shark

shark 官網

Shark是爲 LeakCanary 2 提供支持的堆分析器,它是Kotlin獨立堆分析庫,可在低內存佔用情況下高速運行(PS:LeakCanary 2 之前的堆分析庫是 hahahaha Git地址)。

此處說的 LeakCanary 2betafinal 版本,alpha 版依舊是用的 haha 只不過是用 kotlin 寫的。

Shark 在爲 LeakCanary 2 提供支持的同事也提供 Shark CLI 支持。

Shark 命令行界面(CLI)使您可以直接從計算機分析堆。它可以轉儲安裝在已連接的 Android 設備上的應用程序的堆,對其進行分析,甚至剝離所有敏感數據(例如PII,密碼或加密密鑰)的堆轉儲,這在共享堆轉儲時非常有用。

Shark分析當前應用的內存泄漏情況

shark-cli --device 設備id --process 包名 analyze

shark-cli-analyze

同時支持混淆後的內存泄漏分析,利用mapping文件進行可讀性還原。

shark-cli -d 設備id -p 包名 -m 混淆文件 analyze

shark-cli-analyze-mapping

Shark分析hprof文件

shark-cli -h 生成的hprof文件 analyze

shark-cli-analyze-hprof

Android Profile

Android Profiler分爲三大模塊: cpu內存網絡

官網:使用 Memory Profiler 查看 Java 堆和內存分配

Memory ProfilerAndroid Profiler中的一個組件,它可以幫助您識別內存泄漏和內存溢出,從而導致存根、凍結甚至應用程序崩潰。它顯示了應用程序內存使用的實時圖,讓您捕獲堆轉儲、強制垃圾收集和跟蹤內存分配。

捕獲堆轉儲進行分析

profiler-docs

在列表的頂部,您可以使用右下拉菜單在列表之間切換:

  • Arrange by class: 根據類名分配。
  • Arrange by package:根據包名分配。
  • Arrange by callstack: 根據調用堆棧排序。

查看堆轉儲後的信息:

  • 您的應用程序分配了哪些類型的對象,以及每個對象的數量;
  • 每個對象使用多少內存;
  • 每個對象的引用被保留在你的代碼中;
  • 調用堆棧,用於分配對象的位置(只有在記錄分配時捕獲堆轉儲);

MAT安裝

打開 Eclipse->help->Eclipse Marketplce,搜索Memory Analyze進行安裝,安裝完成後重啓 Eclipse

marketplace-memory-analyze

MAT使用

dump heap 生成的 hprof 文件轉化爲MAT能處理的hprof 文件。

執行 android.os.Debug.dumpHprofData(hprofPath) 生成 hprof 文件,執行之前記得進行GC

hprof-conv 位於 sdk/platform-tools/hprof-conv

hprof-conv memory-android.hprof memory-mat.hprof

MAT處理導入hprof文件

mat-overview

Action 有一下幾個視圖:

視圖 含義
Histogram 列舉內存中對象存在的個數和大小,以及對於的名稱
Dominator Tree 站在對象的角度查看他們的內存情況
Top Consumers 該視圖會顯示可能的內存泄漏點
Duplicate Classes 檢測由多個類加載器加載的類

尋找內存泄漏的類

根據內存中類的對象實例數量,判斷該類對象是否被泄露。

mat-histogram

我們可以利用提供的多種檢索方式進行目標類的檢索,我這裏用包名作爲檢索要素。

Shallow Size

  • 對象自身佔用的內存大小,不包括它引用的對象。
  • 針對非數組類型的對象,它的大小就是對象與它所有的成員變量大小的總和。當然這裏面還會包括一些java語言特性的數據存儲單元。
  • 針對數組類型的對象,它的大小是數組元素對象的大小總和。

Retained Size

Retained Size = 當前對象大小 + 當前對象可直接或間接引用到的對象的大小總和。(間接引用的含義:A->B->C, C就是間接引用。如果BC 沒有被其他對象引用,那麼 RetainedSize-A = ShallowSize(A + B + C) 它和 Dominator 比較相似)
換句話說,Retained Size就是當前對象被GC後,從Heap上總共能釋放掉的內存。
不過,釋放的時候還要排除被GC Roots直接或間接引用的對象。他們暫時不會被被當做Garbage

從上圖可以看出 MainActivityLeakCanaryTestActivityLeakCanaryTestActivity$a 都有一個實例沒有被回收。

分析被泄露的類的引用關係

選擇沒有回收的類,進行 list objects -> with incoming references 操作得到被引用的對象。

mat-histogram-list-object

with outgoing references : 該對象內部引用了那些其他對象;

with incoming references : 該對象被誰進行了引用;

得到被引用的類之後,進行 Path To GC Roots -> exclude all phantom/weak/soft etc. references 操作,得到所有引用類型的引用。

mat-histogram-list-gcroot

StrongReference(強引用):通常我們編寫的代碼都是 StrongReference,於此對應的是強可達性,只有去掉強可達,對象才被回收。

SoftReference(軟引用):只要有足夠的內存,就一直保持對象,直到發現內存喫緊且沒有StrongReference時纔回收對象。一般可用來實現緩存,需要獲取對象時,可以調用get方法。

WeakReference(弱引用):隨時可能會被垃圾回收器回收,不一定要等到虛擬機內存不足時才強制回收。要獲取對象時,同樣可以調用get方法。

PhantomReference(虛引用):根本不會在內存中保持任何對象,你只能使用PhantomReference本身。一般用於在進入finalize()方法後進行特殊的清理過程。

找到最終的泄漏的地方

mat-histogram-list-gcroot-result

從這個圖中我們可以可以得到:

  1. LeakCanaryTestActivity 的一個實例被它的內部類 LeakCanaryTestActivity$Test 的成員變量 context 所持有;
  2. LeakCanaryTestActivity$Test 的一個實例又被 LeakCanaryTestActivity 的成員變量 test 所持有。

Merge對比分析

如果我們沒有明確的目標類,我們可以將兩個 hprof文件(泄漏前、泄漏後) 進行對比。

mat-merge

選擇泄漏之前的 hprof文件 進行對比。

mat-gcroot-merge

對比會得到哪些實例對象數量的增加和減少。如上圖所示對比結果爲 LeakCanaryTestActivityLeakCanaryTestActivity$a (此處的a 爲混淆之後的 Test)兩個類梳理分別增加1個。

我們繼續向上面MAT分析步驟一樣操作:

  1. 進行 list objects -> with incoming references 操作;

  2. 進行 Path To GC Roots -> exclude all phantom/weak/soft etc. references 操作;

mat-merge-result

最終得到的結果和之前分析的相同的。

Jhat-Java自帶的性能監測工具

Java8 jhat Analyzes the Java heap docs

JHatOracle 推出的一款 Hprof 分析軟件,它和 MAT 並稱爲 Java 內存靜態分析利器。不同於 MAT 的單人界面式分析,jHat 使用多人界面式分析。它被 內置在 JDK 中,在命令行中輸入 jhat 命令可查看有沒有相應的命令。

➜  Desktop jhat
ERROR: No arguments supplied
Usage:  jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>

	-J<flag>          Pass <flag> directly to the runtime system. For
			  example, -J-mx512m to use a maximum heap size of 512MB
	-stack false:     Turn off tracking object allocation call stack.
	-refs false:      Turn off tracking of references to objects
	-port <port>:     Set the port for the HTTP server.  Defaults to 7000
	-exclude <file>:  Specify a file that lists data members that should
			  be excluded from the reachableFrom query.
	-baseline <file>: Specify a baseline object dump.  Objects in
			  both heap dumps with the same ID and same class will
			  be marked as not being "new".
	-debug <int>:     Set debug level.
			    0:  No debug output
			    1:  Debug hprof file parsing
			    2:  Debug hprof file parsing, no server
	-version          Report version number
	-h|-help          Print this help and exit
	<file>            The file to read

For a dump file that contains multiple heap dumps,
you may specify which dump in the file
by appending "#<number>" to the file name, i.e. "foo.hprof#3".

All boolean options default to "true"

Jhat 使用的 hprof 文件和 MAT 一樣都需要使用 hprof-conv 進行 hprof 轉化。

使用 Jhat 分析完 hprof 文件後會給一個 Server port ,比如 7000 。那麼我們可以訪問 http://localhost:7000/ 查看分析結果。

jhat-main

以包爲單位展示所有的類,我們下拉到最底部可以看到有其他的查詢方式。

jhat-other-queries

Show heap histogram

我們可以看到對應的類的內存實例數量以及佔用對應的內存大小。

http://localhost:7000/histo/

jhat-histo

Execute Object Query Language (OQL) query

可以使用 OQL 查詢~!

OQL 查詢語法與 Visual VMOQL 類似~ 基本語法如下:

 select <JavaScript expression to select>
         [ from [instanceof] <class name> <identifier>
         [ where <JavaScript boolean expression to filter> ] ]

jhat-oql-result

我們點擊某個類之後可以看到該類的詳細信息:

jhat-class-detail

  • Exclude subclasses 相當於MATwith outgoing references : 該對象內部引用了那些其他對象;

  • Include subclasses 相當於MAT 的 with incoming references : 該對象被誰進行了引用;

jhat-class-instances

先查看類的實例,然後再查看每個實例的相關引用情況。

jhat-class-object

dumpsys meminfo

Android 系統是基於 Linux 內核的操作系統,所以在 Linux 中查看內存使用情況的命令在 Android 手機上也能使用比如 top 命令。除此之外

  • procrank :獲取所有進程的內存使用情況,排序是按照 Pss 大小,詳細輸出每個 PID 對應的 VssRss PssUssSwapPSwapUSwapZSwapcmdline。但該命令使用需要 root 環境。

一般來說內存佔用大小有如下規律:VSS >= RSS >= PSS >= USS

簡稱 全稱 含義 等價
VSS Virtual Set Size 虛擬耗用內存 (包含共享庫佔用的內存)是單個進程全部可訪問的地址空間
RSS Resident Set Size 實際使用物理內存 (包含共享庫佔用的內存)是單個進程實際佔用的內存大小,對於單個共享庫, 儘管無論多少個進程使用,實際該共享庫只會被裝入內存一次。
PSS Proportional Set Size 實際使用的物理內存 (比例分配共享庫佔用的內存)
USS Unique Set Size 進程獨自佔用的物理內存 (不包含共享庫佔用的內存)USS 是一個非常非常有用的數字, 因爲它揭示了運行一個特定進程的真實的內存增量大小。如果進程被終止, USS 就是實際被返還給系統的內存大小。

USS 是針對某個進程開始有可疑內存泄露的情況,進行檢測的最佳數字。懷疑某個程序有內存泄露可以查看這個值是否一直有增加。

  • cat /proc/meminfo :展示系統整體的內存情況,按照內存類型進行分類。
  • free :查看可用內存,缺省單位爲KB。該命令比較簡單、輕量,專注於查看剩餘內存情況。數據來源於 /proc/meminfo

最後一個是本次敘述的重點 dumpsys

dumpsys [options]
               meminfo 顯示內存信息
               cpuinfo 顯示CPU信息
               account 顯示accounts信息
               activity 顯示所有的activities的信息
               window 顯示鍵盤,窗口和它們的關係
               wifi 顯示wifi信息

使用 dumpysys meminfo 查看內存信息,後面可以添加 pid | packagename 查看該應用程序的內存信息。

~/Desktop adb shell dumpsys meminfo com.tzx.androidcode
Applications Memory Usage (in Kilobytes):
Uptime: 131873995 Realtime: 240892295

** MEMINFO in pid 19924 [com.tzx.androidcode] **
                   Pss  Private  Private  SwapPss     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap    11062    11032        0       99    38912    21656    17255
  Dalvik Heap     4079     3984        0        0     5638     2819     2819
 Dalvik Other     1405     1404        0        1
        Stack       64       64        0        0
       Ashmem        2        0        0        0
      Gfx dev     2052     2052        0        0
    Other dev        8        0        8        0
     .so mmap      959       80       64        4
    .jar mmap     1062        0       24        0
    .apk mmap      109        0        0        0
    .ttf mmap       33        0        0        0
    .dex mmap     4513       36     4392        0
    .oat mmap      197        0        0        0
    .art mmap     6229     5924       16        1
   Other mmap      680      196       96        0
   EGL mtrack    19320    19320        0        0
    GL mtrack     6392     6392        0        0
      Unknown     1106     1092        0       15
        TOTAL    59392    51576     4600      120    44550    24475    20074

 App Summary
                       Pss(KB)
                        ------
           Java Heap:     9924
         Native Heap:    11032
                Code:     4596
               Stack:       64
            Graphics:    27764
       Private Other:     2796
              System:     3216

               TOTAL:    59392       TOTAL SWAP PSS:      120

 Objects
               Views:       82         ViewRootImpl:        2
         AppContexts:        8           Activities:        2
              Assets:       11        AssetManagers:        0
       Local Binders:       22        Proxy Binders:       41
       Parcel memory:       10         Parcel count:       24
    Death Recipients:        2      OpenSSL Sockets:        0
            WebViews:        0

 SQL
         MEMORY_USED:        0
  PAGECACHE_OVERFLOW:        0          MALLOC_SIZE:        0

Android 程序內存被分爲2部分:native虛擬機虛擬機 就是我們平常說的 java堆,我們創建的對象是在這裏面分配的,而 bitmap 是直接在 native 上分配的,對於內存的限制是native+dalvik 不能超過最大限制。以上信息可以看到該應用程序佔用的 nativedalvik,對於分析內存泄露,內存溢出都有極大的作用。

讀取垃圾回收消息(GC Log)

官網:讀取垃圾回收消息

Dalvik 日誌消息

Dalvik(而不是 ART)中,每個 GC 都會將以下信息輸出到 logcat 中:

D/dalvikvm(PID): GC_Reason Amount_freed, Heap_stats, External_memory_stats, Pause_time

示例:

D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms

ART 日誌消息

Dalvik 不同,ART 不會爲未明確請求的 GC 記錄消息。只有在系統認爲 GC 速度較慢時纔會輸出 GC 消息。更確切地說,僅在 GC 暫停時間超過 5 毫秒或 GC 持續時間超過 100 毫秒時。如果應用未處於可察覺到暫停的狀態(例如應用在後臺運行時,這種情況下,用戶無法察覺 GC 暫停),則其所有 GC 都不會被視爲速度較慢。系統一直會記錄顯式 GC

ART 會在其垃圾回收日誌消息中包含以下信息:

I/art: GC_Reason GC_Name Objects_freed(Size_freed) AllocSpace Objects,
        Large_objects_freed(Large_object_size_freed) Heap_stats LOS objects, Pause_time(s)

示例:

I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects,
        21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms

文章到這裏就全部講述完啦,若有其他需要交流的可以留言哦!!

想閱讀作者的更多文章,可以查看我 個人博客 和公共號:

振興書城

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章