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
的原理很簡單: 在 Activity
或 Fragment
被銷燬後, 將他們的引用包裝成一個 WeakReference
, 然後將這個 WeakReference
關聯到一個 ReferenceQueue
。查看ReferenceQueue
中是否含有 Activity
或 Fragment
的引用。如果沒有 觸發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
升級到 2.0
的 beta
和 final
版本之後 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
是爲 LeakCanary 2
提供支持的堆分析器,它是Kotlin
獨立堆分析庫,可在低內存佔用情況下高速運行(PS:LeakCanary 2
之前的堆分析庫是 haha
,haha Git地址)。
此處說的 LeakCanary 2
爲 beta
和 final
版本,alpha
版依舊是用的 haha
只不過是用 kotlin
寫的。
Shark
在爲 LeakCanary 2
提供支持的同事也提供 Shark CLI 支持。
Shark
命令行界面(CLI
)使您可以直接從計算機分析堆。它可以轉儲安裝在已連接的 Android
設備上的應用程序的堆,對其進行分析,甚至剝離所有敏感數據(例如PII,密碼或加密密鑰)的堆轉儲,這在共享堆轉儲時非常有用。
Shark分析當前應用的內存泄漏情況
shark-cli --device 設備id --process 包名 analyze
同時支持混淆後的內存泄漏分析,利用mapping
文件進行可讀性還原。
shark-cli -d 設備id -p 包名 -m 混淆文件 analyze
Shark分析hprof文件
shark-cli -h 生成的hprof文件 analyze
Android Profile
Android Profiler分爲三大模塊: cpu、內存 、網絡。
官網:使用 Memory Profiler 查看 Java 堆和內存分配
Memory Profiler
是Android Profiler
中的一個組件,它可以幫助您識別內存泄漏和內存溢出,從而導致存根、凍結甚至應用程序崩潰。它顯示了應用程序內存使用的實時圖,讓您捕獲堆轉儲、強制垃圾收集和跟蹤內存分配。
捕獲堆轉儲進行分析
在列表的頂部,您可以使用右下拉菜單在列表之間切換:
Arrange by class
: 根據類名分配。Arrange by package
:根據包名分配。Arrange by callstack
: 根據調用堆棧排序。
查看堆轉儲後的信息:
- 您的應用程序分配了哪些類型的對象,以及每個對象的數量;
- 每個對象使用多少內存;
- 每個對象的引用被保留在你的代碼中;
- 調用堆棧,用於分配對象的位置(只有在記錄分配時捕獲堆轉儲);
MAT安裝
打開 Eclipse->help->Eclipse Marketplce
,搜索Memory Analyze
進行安裝,安裝完成後重啓 Eclipse
。
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文件
Action
有一下幾個視圖:
視圖 | 含義 |
---|---|
Histogram | 列舉內存中對象存在的個數和大小,以及對於的名稱 |
Dominator Tree | 站在對象的角度查看他們的內存情況 |
Top Consumers | 該視圖會顯示可能的內存泄漏點 |
Duplicate Classes | 檢測由多個類加載器加載的類 |
尋找內存泄漏的類
根據內存中類的對象實例數量,判斷該類對象是否被泄露。
我們可以利用提供的多種檢索方式進行目標類的檢索,我這裏用包名作爲檢索要素。
Shallow Size
- 對象自身佔用的內存大小,不包括它引用的對象。
- 針對非數組類型的對象,它的大小就是對象與它所有的成員變量大小的總和。當然這裏面還會包括一些
java
語言特性的數據存儲單元。 - 針對數組類型的對象,它的大小是數組元素對象的大小總和。
Retained Size
Retained Size
= 當前對象大小
+ 當前對象可直接或間接引用到的對象的大小總和
。(間接引用的含義:A->B->C, C就是間接引用。如果B
和 C
沒有被其他對象引用,那麼 RetainedSize-A = ShallowSize(A + B + C)
它和 Dominator 比較相似)
換句話說,Retained Size
就是當前對象被GC
後,從Heap
上總共能釋放掉的內存。
不過,釋放的時候還要排除被GC Roots
直接或間接引用的對象。他們暫時不會被被當做Garbage
。
從上圖可以看出 MainActivity
、LeakCanaryTestActivity
和 LeakCanaryTestActivity$a
都有一個實例沒有被回收。
分析被泄露的類的引用關係
選擇沒有回收的類,進行 list objects -> with incoming references
操作得到被引用的對象。
with outgoing references
: 該對象內部引用了那些其他對象;
with incoming references
: 該對象被誰進行了引用;
得到被引用的類之後,進行 Path To GC Roots -> exclude all phantom/weak/soft etc. references
操作,得到所有引用類型的引用。
StrongReference
(強引用):通常我們編寫的代碼都是StrongReference
,於此對應的是強可達性,只有去掉強可達,對象才被回收。
SoftReference
(軟引用):只要有足夠的內存,就一直保持對象,直到發現內存喫緊且沒有StrongReference
時纔回收對象。一般可用來實現緩存,需要獲取對象時,可以調用get方法。
WeakReference
(弱引用):隨時可能會被垃圾回收器回收,不一定要等到虛擬機內存不足時才強制回收。要獲取對象時,同樣可以調用get
方法。
PhantomReference
(虛引用):根本不會在內存中保持任何對象,你只能使用PhantomReference
本身。一般用於在進入finalize()
方法後進行特殊的清理過程。
找到最終的泄漏的地方
從這個圖中我們可以可以得到:
LeakCanaryTestActivity
的一個實例被它的內部類LeakCanaryTestActivity$Test
的成員變量context
所持有;LeakCanaryTestActivity$Test
的一個實例又被LeakCanaryTestActivity
的成員變量test
所持有。
Merge對比分析
如果我們沒有明確的目標類,我們可以將兩個 hprof文件(泄漏前、泄漏後)
進行對比。
選擇泄漏之前的 hprof文件
進行對比。
對比會得到哪些實例對象數量的增加和減少。如上圖所示對比結果爲 LeakCanaryTestActivity
和 LeakCanaryTestActivity$a
(此處的a
爲混淆之後的 Test
)兩個類梳理分別增加1個。
我們繼續向上面MAT分析步驟一樣操作:
-
進行
list objects -> with incoming references
操作; -
進行
Path To GC Roots -> exclude all phantom/weak/soft etc. references
操作;
最終得到的結果和之前分析的相同的。
Jhat-Java自帶的性能監測工具
Java8 jhat Analyzes the Java heap docs
JHat
是 Oracle
推出的一款 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/
查看分析結果。
以包爲單位展示所有的類,我們下拉到最底部可以看到有其他的查詢方式。
Show heap histogram
我們可以看到對應的類的內存實例數量以及佔用對應的內存大小。
http://localhost:7000/histo/
Execute Object Query Language (OQL) query
可以使用 OQL
查詢~!
OQL
查詢語法與 Visual VM
的 OQL
類似~ 基本語法如下:
select <JavaScript expression to select>
[ from [instanceof] <class name> <identifier>
[ where <JavaScript boolean expression to filter> ] ]
我們點擊某個類之後可以看到該類的詳細信息:
-
Exclude subclasses
相當於MAT 的with outgoing references
: 該對象內部引用了那些其他對象; -
Include subclasses
相當於MAT 的with incoming references
: 該對象被誰進行了引用;
先查看類的實例,然後再查看每個實例的相關引用情況。
dumpsys meminfo
Android
系統是基於 Linux
內核的操作系統,所以在 Linux
中查看內存使用情況的命令在 Android
手機上也能使用比如 top
命令。除此之外
procrank
:獲取所有進程的內存使用情況,排序是按照Pss
大小,詳細輸出每個PID
對應的Vss
、Rss
Pss
、Uss
、Swap
、PSwap
、USwap
、ZSwap
和cmdline
。但該命令使用需要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
不能超過最大限制。以上信息可以看到該應用程序佔用的 native
和 dalvik
,對於分析內存泄露,內存溢出都有極大的作用。
讀取垃圾回收消息(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
文章到這裏就全部講述完啦,若有其他需要交流的可以留言哦!!
想閱讀作者的更多文章,可以查看我 個人博客 和公共號: