什么是內存泄露
一些對象有著有限的生命周期。當這些對象所要做的事情完成了,我們希望他們會被回收掉。但是如果有一系列對這個對象的引用,那么在我們期待這個對象生命周期結束的時候被收回的時候,它是不會被回收的。它還會占用內存,這就造成了內存泄露。持續累加,內存很快被耗盡。
比如,當 Activity.onDestroy被調用之后,activity 以及它涉及到的 view 和相關的 bitmap 都應該被回收。但是,如果有一個后臺線程持有這個 activity 的引用,那么 activity 對應的內存就不能被回收。這最終將會導致內存耗盡,然后因為 OOM 而 crash。
LeakCanary
LeakCanary 是Square公司開發的一個用于檢測OOM(out of memory的縮寫)問題的開源庫。你可以在 debug 包種輕松檢測內存泄露。
Github地址:https://github.com/square/leakcanary
如何使用
引入LeakCanary庫 ,在項目的build.gradle文件添加:
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
gradle 強大的可配置性,可以確保只在編譯 debug 版本時才會檢查內存泄露,而編譯 release 等版本的時候則會自動跳過檢查,避免影響性能。
在自定義的Application中初始化 ,LeakCanary.install()會返回一個預定義的 RefWatcher,同時也會啟用一個 ActivityRefWatcher,用于自動監控調用 Activity.onDestroy()之后泄的activity。
如果只想檢測Activity的內存泄露,只需要添加這一行代碼 。
private static RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
//LeakCanary 就會自動偵測 activity 的內存泄露
// 會返回一個預定義的 RefWatcher ,同時也會啟用一個 ActivityRefWatcher,用于自動監控調用 Activity.onDestroy() 之后泄露的 activity。
refWatcher = LeakCanary.install(this);
}
如果還想檢測fragment
public class BaseFragment extends Fragment {
//使用 RefWatcher 監控 Fragment:
@Override
public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = MyApplication.getRefWatcher();
refWatcher.watch(this);
}
}
當你在測試debug版本過程中出現內存泄露時,LeakCanary將會自動展示一個通知欄
通過通知里的信息,我們可以解決很多內存泄露問題 。
備注:LeakCanary只支持4.0以上,原因是其中在watch 每個Activity時調用了Application的registerActivityLifecycleCallback函數,這個函數只在4.0上才支持,但是在4.0以下也是可以用的,可以在Application中將返回的RefWatcher存下來,然后在基類Activity的onDestroy函數中調用。
工作原理
簡單概述一下, 源碼還沒有分析明白 。
- RefWatcher.watch()創建一個 KeyedWeakReference 到要被監控的對象 (也就是弱引用)。
- 然后在后臺線程檢查引用是否被清除,如果沒有,調用GC。
- 如果引用還是未被清除,把 heap 內存 dump 到 APP 對應的文件系統中的一個 .hprof文件中。
- 在另外一個進程中的 HeapAnalyzerService有一個 HeapAnalyzer使用HAHA 解析這個文件。得益于唯一的 reference key, HeapAnalyzer找到 KeyedWeakReference,定位內存泄露。
- HeapAnalyzer計算 到 GC roots 的最短強引用路徑,并確定是否是泄露。如果是的話,建立導致泄露的引用鏈。
- 引用鏈傳遞到 APP 進程中的 DisplayLeakService, 并以通知的形式展示出來。
源碼層面簡單分析
RefWatch
ReftWatcher是leakcancay檢測內存泄露的發起點。使用方法為,在對象生命周期即將結束的時候,調用
RefWatcher.watch(Object object)
為了達到檢測內存泄露的目的,RefWatcher需要
private final Executor watchExecutor;
private final DebuggerControl debuggerControl;
private final GcTrigger gcTrigger;
private final HeapDumper heapDumper;
private final Set<String> retainedKeys;
private final ReferenceQueue<Object> queue;
private final HeapDump.Listener heapdumpListener;
- watchExecutor: 執行內存泄露檢測的executor
- debuggerControl :用于查詢是否正在調試中,調試中不會執行內存泄露檢測
- gcTrigger: 用于在判斷內存泄露之前,再給一次GC的機會
- headDumper: 用于在產生內存泄露室執行dump 內存heap
- retainedKeys: 持有那些呆檢測以及產生內存泄露的引用的key
- queue : 用于判斷弱引用所持有的對象是否已被GC。
- heapdumpListener: 用于分析前面產生的dump文件,找到內存泄露的原因
接下來,我們來看看watch函數背后是如何利用這些工具,生成內存泄露分析報告的。
/**
* Watches the provided references and checks if it can be GCed. This method is non blocking,
* the check is done on the {@link Executor} this {@link RefWatcher} has been constructed with.
*
* @param referenceName An logical identifier for the watched object.
*/
public void watch(Object watchedReference, String referenceName) {
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
// 如果處于debug模式,直接return
if (debuggerControl.isDebuggerAttached()) {
return;
}
//記住開始觀測的時間
final long watchStartNanoTime = System.nanoTime();
//生成一個隨機的key,并加入set中
String key = UUID.randomUUID().toString();
retainedKeys.add(key);
//生成一個KeyedWeakReference
final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
//調用watchExecutor,執行內存泄露的檢測
watchExecutor.execute(new Runnable() {
@Override public void run() {
ensureGone(reference, watchStartNanoTime);
}
});
}
所以最后的核心函數是在ensureGone這個方法里面。要理解其工作原理,就得從keyedWeakReference說起
WeakReference與ReferenceQueue
從watch函數中,可以看到,每次檢測對象內存是否泄露時,我們都會生成一個KeyedReferenceQueue,這個類其實就是一個WeakReference,只不過其額外附帶了一個key和一個name
/** @see {@link HeapDump#referenceKey}. */
final class KeyedWeakReference extends WeakReference<Object> {
public final String key;
public final String name;
KeyedWeakReference(Object referent, String key, String name,
ReferenceQueue<Object> referenceQueue) {
super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
this.key = checkNotNull(key, "key");
this.name = checkNotNull(name, "name");
}
}
在構造時我們需要傳入一個ReferenceQueue,這個ReferenceQueue是直接傳入了WeakReference中,關于這個類,有興趣的可以直接看Reference的源碼。我們這里需要知道的是,每次WeakReference所指向的對象被GC后,這個弱引用都會被放入這個與之相關聯的ReferenceQueue隊列中。
在reference類加載的時候,java虛擬機會創建一個最大優先級的后臺線程,這個線程的工作原理就是不斷檢測pending是否為null,如果不為null,就將其放入ReferenceQueue中,pending不為null的情況就是,引用所指向的對象已被GC,變為不可達。
那么只要我們在構造弱引用的時候指定了ReferenceQueue,每當弱引用所指向的對象被內存回收的時候,我們就可以在queue中找到這個引用。如果我們期望一個對象被回收,那如果在接下來的預期時間之后,我們發現它依然沒有出現在ReferenceQueue中,那就可以判定它的內存泄露了。LeakCanary檢測內存泄露的核心原理就在這里。
監測時機
什么時候去檢測能判定內存泄露呢?這個可以看AndroidWatchExecutor的實現
public final class AndroidWatchExecutor implements Executor {
private final Handler backgroundHandler;
public AndroidWatchExecutor() {
mainHandler = new Handler(Looper.getMainLooper());
HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
handlerThread.start();
backgroundHandler = new Handler(handlerThread.getLooper());
}
....
private void executeDelayedAfterIdleUnsafe(final Runnable runnable) {
// This needs to be called from the main thread.
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override public boolean queueIdle() {
backgroundHandler.postDelayed(runnable, DELAY_MILLIS);
return false;
}
});
}
}
這里又看到一個比較少的用法,IdleHandler,IdleHandler的原理就是在messageQueue因為空閑等待消息時給使用者一個hook。那AndroidWatchExecutor會在主線程空閑的時候,派發一個后臺任務,這個后臺任務會在DELAY_MILLIS時間之后執行。LeakCanary設置的是5秒。
二次確認保證內存泄露準確性
為了避免因為gc不及時帶來的誤判,leakcanay會進行二次確認進行保證。
void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
//計算從調用watch到進行檢測的時間段
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
//根據queue移除已被GC的對象的弱引用
removeWeaklyReachableReferences();
//如果內存已被回收或者處于debug模式,直接返回
if (gone(reference) || debuggerControl.isDebuggerAttached()) {
return;
}
//如果內存依舊沒被釋放,則再給一次gc的機會
gcTrigger.runGc();
//再次移除
removeWeaklyReachableReferences();
if (!gone(reference)) {
//走到這里,認為內存確實泄露了
long startDumpHeap = System.nanoTime();
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = heapDumper.dumpHeap();
if (heapDumpFile == null) {
// Could not dump the heap, abort.
return;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, watchDurationMs, gcDurationMs,
heapDumpDurationMs));
}
}
Dump Heap
監測到內存泄露后,首先做的就是dump出當前的heap,默認的AndroidHeapDumper調用的是
Debug.dumpHprofData(filePath);
導出當前內存的hprof分析文件,一般我們在DeviceMonitor中也可以dump出hprof文件,然后將其從dalvik格式轉成標準jvm格式,然后使用MAT進行分析。
那么LeakCanary是如何分析內存泄露的呢?
HaHa
LeakCanary 分析內存泄露用到了一個和Mat類似的工具叫做HaHa,使用HaHa的方法如下:
public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
long analysisStartNanoTime = System.nanoTime();
if (!heapDumpFile.exists()) {
Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
return failure(exception, since(analysisStartNanoTime));
}
try {
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
HprofParser parser = new HprofParser(buffer);
Snapshot snapshot = parser.parse();
Instance leakingRef = findLeakingReference(referenceKey, snapshot);
// False alarm, weak reference was cleared in between key check and heap dump.
if (leakingRef == null) {
return noLeak(since(analysisStartNanoTime));
}
return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
} catch (Throwable e) {
return failure(e, since(analysisStartNanoTime));
}
}
返回的ActivityResult對象中包含了對象到GC root的最短路徑。LeakCanary在dump出hprof文件后,會啟動一個IntentService進行分析:HeapAnalyzerService在分析出結果之后會啟動DisplayLeakService用來發起Notification 以及將結果記錄下來寫在文件里面。以后每次啟動LeakAnalyzerActivity就從文件里讀取歷史結果。
參考文檔
LeakCanary 中文使用說明
LeakCanary 內存泄露監測原理研究 : 結合源碼分析leakcanary檢查內存泄露的過程。