LeakCanary(二)記憶體洩露監測原理研究
LeakCanary 記憶體洩露監測原理研究
"Read the fucking source code" -- linus一句名言體現出了閱讀原始碼的重要性,學習別人得程式碼是提升自己的重要途徑。最近用到了LeakCanary,順便看一下其程式碼,學習一下。
LeakCanary是安卓中用來檢測記憶體洩露的小工具,它能幫助我們提早發現程式碼中隱藏的bug, 降低應用中記憶體洩露以及OOM產生的概率。
總體流程
在LeakCanary
中,檢測主要分為三步:1.檢測一個物件是否是可疑的洩漏物件;2.如果第一步發現可疑物件,dump
記憶體快照,通過分析.hprof
檔案,確定懷疑的物件是否真的洩漏。3.將分析的結果展示
廢話不多說,關於LeakCanary的使用方法,其實很簡單,如果我們只想檢測Activity的記憶體洩露,而且只想使用其預設的報告方式,我們只需要在Application中加一行程式碼,
LeakCanary.install(this);
那我們今天閱讀原始碼的切入點,就從這個靜態方法開始。
/**
* Creates a {@link RefWatcher} that works out of the box, and starts watching activity
* references (on ICS+).
*/
public static RefWatcher install(Application application) {
return install(application, DisplayLeakService.class,
AndroidExcludedRefs.createAppDefaults().build());
}
這個函式內部直接呼叫了另外一個過載的函式
/**
* Creates a {@link RefWatcher} that reports results to the provided service, and starts watching
* activity references (on ICS+).
*/
public static RefWatcher install(Application application,
Class<? extends AbstractAnalysisResultService> listenerServiceClass,
ExcludedRefs excludedRefs) {
//判斷是否在Analyzer程式裡
if (isInAnalyzerProcess(application)) {
return RefWatcher.DISABLED;
}
enableDisplayLeakActivity(application);
HeapDump.Listener heapDumpListener =
new ServiceHeapDumpListener(application, listenerServiceClass);
RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs);
ActivityRefWatcher.installOnIcsPlus(application, refWatcher);
return refWatcher;
}
因為leakcanay會開啟一個遠端service用來分析每次產生的記憶體洩露,而安卓的應用每次開啟程式都會呼叫Applicaiton的onCreate方法,因此我們有必要預先判斷此次Application啟動是不是在analyze service啟動時,
public static boolean isInServiceProcess(Context context, Class<? extends Service> serviceClass) {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo;
try {
packageInfo = packageManager.getPackageInfo(context.getPackageName(), GET_SERVICES);
} catch (Exception e) {
Log.e("AndroidUtils", "Could not get package info for " + context.getPackageName(), e);
return false;
}
String mainProcess = packageInfo.applicationInfo.processName;
ComponentName component = new ComponentName(context, serviceClass);
ServiceInfo serviceInfo;
try {
serviceInfo = packageManager.getServiceInfo(component, 0);
} catch (PackageManager.NameNotFoundException ignored) {
// Service is disabled.
return false;
}
if (serviceInfo.processName.equals(mainProcess)) {
Log.e("AndroidUtils",
"Did not expect service " + serviceClass + " to run in main process " + mainProcess);
// Technically we are in the service process, but we're not in the service dedicated process.
return false;
}
//查詢當前程式名
int myPid = android.os.Process.myPid();
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.RunningAppProcessInfo myProcess = null;
for (ActivityManager.RunningAppProcessInfo process : activityManager.getRunningAppProcesses()) {
if (process.pid == myPid) {
myProcess = process;
break;
}
}
if (myProcess == null) {
Log.e("AndroidUtils", "Could not find running process for " + myPid);
return false;
}
return myProcess.processName.equals(serviceInfo.processName);
}
判斷Application是否是在service程式裡面啟動,最直接的方法就是判斷當前程式名和service所屬的程式是否相同。當前程式名的獲取方式是使用ActivityManager的getRunningAppProcessInfo方法,找到程式pid與當前程式pid相同的程式,然後從中拿到processName. service所屬程式名。獲取service應處程式的方法是用PackageManager的getPackageInfo方法。
RefWatcher
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;
private final ExcludedRefs excludedRefs;
- watchExecutor: 執行記憶體洩露檢測的executor
- debuggerControl :用於查詢是否正在除錯中,除錯中不會執行記憶體洩露檢測
- queue : 用於判斷弱引用所持有的物件是否已被GC。
- gcTrigger: 用於在判斷記憶體洩露之前,再給一次GC的機會
- headDumper: 用於在產生記憶體洩露室執行dump 記憶體heap
- heapdumpListener: 用於分析前面產生的dump檔案,找到記憶體洩露的原因
- excludedRefs: 用於排除某些系統bug導致的記憶體洩露
- retainedKeys: 持有那些呆檢測以及產生記憶體洩露的引用的key。
接下來,我們來看看watch函式背後是如何利用這些工具,生成記憶體洩露分析報告的。
public void watch(Object watchedReference, String referenceName) {
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
//如果處於debug模式,則直接返回
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這個runnable裡面。要理解其工作原理,就得從keyedWeakReference說起
WeakReference與ReferenceQueue
從watch函式中,可以看到,每次檢測物件記憶體是否洩露時,我們都會生成一個KeyedReferenceQueue,這個類其實就是一個WeakReference,只不過其額外附帶了一個key和一個name
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佇列中。
我們這裡可以貼下其核心程式碼
private static class ReferenceHandler extends Thread {
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
for (;;) {
Reference<Object> r;
synchronized (lock) {
if (pending != null) {
r = pending;
pending = r.discovered;
r.discovered = null;
} else {
//....
try {
try {
lock.wait();
} catch (OutOfMemoryError x) { }
} catch (InterruptedException x) { }
continue;
}
}
// Fast path for cleaners
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
ReferenceQueue<Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
}
在reference類載入的時候,java虛擬機器會建立一個最大優先順序的後臺執行緒,這個執行緒的工作原理就是不斷檢測pending是否為null,如果不為null,就將其放入ReferenceQueue中,pending不為null的情況就是,引用所指向的物件已被GC,變為不可達。
那麼只要我們在構造弱引用的時候指定了ReferenceQueue,每當弱引用所指向的物件被記憶體回收的時候,我們就可以在queue中找到這個引用。如果我們期望一個物件被回收,那如果在接下來的預期時間之後,我們發現它依然沒有出現在ReferenceQueue中,那就可以判定它的記憶體洩露了。LeakCanary檢測記憶體洩露的核心原理就在這裡。
其實Java裡面的WeakHashMap裡也用到了這種方法,來判斷hash表裡的某個鍵值是否還有效。在構造WeakReference的時候給其指定了ReferenceQueue.
監測時機
什麼時候去檢測能判定記憶體洩露呢?這個可以看AndroidWatchExecutor的實現
public final class AndroidWatchExecutor implements Executor {
//....
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);
//dump出heap報告
File heapDumpFile = heapDumper.dumpHeap();
if (heapDumpFile == HeapDumper.NO_DUMP) {
// Could not dump the heap, abort.
return;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
gcDurationMs, heapDumpDurationMs));
}
}
private boolean gone(KeyedWeakReference reference) {
return !retainedKeys.contains(reference.key);
}
private void removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
KeyedWeakReference ref;
while ((ref = (KeyedWeakReference) queue.poll()) != null) {
retainedKeys.remove(ref.key);
}
}
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));
}
}
關於HaHa的原理,感興趣的同學可以深究,這裡就不深入介紹了。
返回的ActivityResult物件中包含了物件到GC root的最短路徑。LeakCanary在dump出hprof檔案後,會啟動一個IntentService進行分析:HeapAnalyzerService在分析出結果之後會啟動DisplayLeakService用來發起Notification 以及將結果記錄下來寫在檔案裡面。以後每次啟動LeakAnalyzerActivity就從檔案裡讀取歷史結果。
ExcludedRef
由於某些系統的bug,以及某些廠商rom的bug,Activity在finish之後仍然會被某些系統元件給hold住。LeakCanary列出了一些很常見的,比如三星的手機activity會被audioManager給hold住,試了一下huawei的系統貌似也會出現,還有比如activity中如果有會獲取鍵盤焦點的view,在activity finish之後view會被InputMethodManager給hold住,因為view會持有activity 造成activity洩漏,除非有新的view獲取鍵盤焦點。
LeakCanary中有一個AndroidExcludedRefs列舉類,其中列舉了很多特定版本系統issue引起的記憶體洩漏,因為這種問題 不是開發者導致的,因此HeapAnalyzerService在分析記憶體洩露時,會將這些GC Root排除在外。而且每個ExcludedRef通常都跟特定廠商或者Android版本有關,這些列舉類都加了一個適用條件。
AndroidExcludedRefs(boolean applies) { this.applies = applies;}
AUDIO_MANAGER__MCONTEXT_STATIC(SAMSUNG.equals(MANUFACTURER) && SDK_INT == KITKAT) {
@Override void add(ExcludedRefs.Builder excluded) {
// Samsung added a static mContext_static field to AudioManager, holds a reference to the
// activity.
// Observed here: https://github.com/square/leakcanary/issues/32
excluded.staticField("android.media.AudioManager", "mContext_static");
}
},
比如上面這個AudioManager引起的問題,只有在Build中的MANUFACTURER表明是三星以及sdk版本是KITKAT(4.4, 19)時才適用。
手動釋放資源
然後並不是leakCanary不報錯我們就不用管,activity記憶體洩露了,大部分情況下沒多大事,但是有些佔用記憶體很多的頁面,比如相簿,webview頁面,因為acitivity不能回收,它所指向的view以及view下面的bitmap都不能被回收,這是會造成很不好的後果的,很可能會導致OOM,因此我們需要手動在Activity結束時回收資源。
Under 4.0 & Fragment
LeakCanary只支援4.0以上,原因是其中在watch 每個Activity時適用了Application的registerActivityLifecycleCallback函式,這個函式只在4.0上才支援,但是在4.0以下也是可以用的,可以在Application中將返回的RefWatcher存下來,然後在基類Activity的onDestroy函式中呼叫。
同理,如果我們想檢測Fragment的記憶體的話,我們也闊以在Fragment的onDestroy中watch它。
相關文章
- LeakCanary 傻瓜式的記憶體洩露檢測工具記憶體洩露
- LeakCanary傻瓜式的記憶體洩露檢測工具記憶體洩露
- Android檢測記憶體洩漏之leakcanaryAndroid記憶體
- 007 LeakCanary 記憶體洩漏原理完全解析記憶體
- 記憶體洩露記憶體洩露
- Android 檢測記憶體洩露Android記憶體洩露
- MFC記憶體洩露與檢測記憶體洩露
- Android 記憶體洩漏檢測工具 LeakCanary(Kotlin版)的實現原理Android記憶體Kotlin
- js記憶體洩露JS記憶體洩露
- JavaScript記憶體洩露JavaScript記憶體洩露
- 記憶體洩露嗎記憶體洩露
- SHBrowseForFolder 記憶體洩露記憶體洩露
- 記憶體溢位和記憶體洩露記憶體溢位記憶體洩露
- Lowmemorykiller記憶體洩露分析記憶體洩露
- Android記憶體優化——記憶體洩露檢測分析方法Android優化記憶體洩露
- C程式記憶體洩露檢測工具——ValgrindC程式記憶體洩露
- 在iOS上自動檢測記憶體洩露iOS記憶體洩露
- Java記憶體問題 及 LeakCanary 原理分析Java記憶體
- MLeaksFinder:精準 iOS 記憶體洩露檢測工具iOS記憶體洩露
- 使用 mtrace 分析 “記憶體洩露”記憶體洩露
- 實戰Go記憶體洩露Go記憶體洩露
- js記憶體洩露的原因JS記憶體洩露
- Java記憶體洩露的原因Java記憶體洩露
- JAVA 記憶體洩露的理解Java記憶體洩露
- IE中的記憶體洩露記憶體洩露
- 學習Java:記憶體洩露Java記憶體洩露
- 記一次Go websocket 專案記憶體洩露排查 + 使用Go pprof定位記憶體洩露GoWeb記憶體洩露
- Android 記憶體洩露詳解Android記憶體洩露
- 線上記憶體洩露定位--memleak工具記憶體洩露
- Pprof定位Go程式記憶體洩露Go記憶體洩露
- 如何處理 JavaScript 記憶體洩露JavaScript記憶體洩露
- leaks工具查詢記憶體洩露記憶體洩露
- 記憶體洩露引起的問題記憶體洩露
- 如何定位和解決記憶體洩露記憶體洩露
- JavaScript中的記憶體洩露模式JavaScript記憶體洩露模式
- ThreaLocal記憶體洩露的問題記憶體洩露
- JVM與記憶體洩露問題JVM記憶體洩露
- JVM記憶體洩露(OOM)!帶你一一揭秘【第二彈】JVM記憶體洩露OOM