分析記憶體洩漏分析工具
核心一是ReferenceQueue,這是一個用來儲存Reference的佇列,具體見WeakReference的構造方法:
public class WeakReference<T> extends Reference<T> {
public WeakReference(T var1) {
super(var1);
}
public WeakReference(T var1, ReferenceQueue<? super T> var2) {
super(var1, var2);
}
}複製程式碼
先了解WeakReference和ReferenceQueue:
WeakReference和ReferenceQueue
首先需要知道WeakReference。當GC執行緒掃描它所管轄的記憶體區域時,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否(這一點與SoftReference不同),都會回收它的記憶體。由於垃圾回收器是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件。
WeakReference和ReferenceQueue聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。上圖中的實線a表示強引用,虛線aa表示弱引用。如果切斷a,那麼Object物件將會被回收。正如下面這段測試程式碼所示。
A a = new A();
ReferenceQueue queue = new ReferenceQueue();
WeakReference aa = new WeakReference(a, queue);
a = null;
Runtime.getRuntime().gc();
System.runFinalization();
try {
} catch (InterruptedException e) {
e.printStackTrace();
}
Reference poll = null;
while ((poll = queue.poll()) != null) {
System.out.println(poll.toString());
}
複製程式碼
當垃圾回收器回收物件的時候,aa
這個弱引用將會入隊進入ReferenceQueue
,所以queue.poll()
將不會為空,除非這個物件沒有被垃圾回收器清理。ok了,開始分析程式。
程式入口
public static RefWatcher install(Application application) {
return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
.excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
.buildAndInstall();
}複製程式碼
關鍵在buildAndInstall這個方法
public RefWatcher buildAndInstall() {
RefWatcher refWatcher = build();
if (refWatcher != DISABLED) {
LeakCanary.enableDisplayLeakActivity(context);
ActivityRefWatcher.install((Application) context, refWatcher);//關鍵程式碼
}
return refWatcher;
}複製程式碼
看install
public static void install(Application application, RefWatcher refWatcher) {
new ActivityRefWatcher(application, refWatcher).watchActivities();
}複製程式碼
看watchActivities
public void watchActivities() {
// Make sure you don't get installed twice.
stopWatchingActivities();
application.registerActivityLifecycleCallbacks(lifecycleCallbacks);//4.0提供的生命週期的監測
}複製程式碼
看lifecycleCallbacks
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
new Application.ActivityLifecycleCallbacks() {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override public void onActivityStarted(Activity activity) {
}
@Override public void onActivityResumed(Activity activity) {
}
@Override public void onActivityPaused(Activity activity) {
}
@Override public void onActivityStopped(Activity activity) {
}
@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override public void onActivityDestroyed(Activity activity) {
ActivityRefWatcher.this.onActivityDestroyed(activity);//在Activity Destroy的時候去監測記憶體洩漏
}
};複製程式碼
看onActivityDestroyed:
void onActivityDestroyed(Activity activity) {
refWatcher.watch(activity);
}複製程式碼
到這裡,可以看到leakcanary在4.0以上系統的監測是通過ActivityLifecycleCallbacks來監測的,監測時機是在onActivityDestroyed。具體監測方法是在refWatcher,繼續往下看。
refWatcher#watch
public void watch(Object watchedReference, String referenceName) {
if (this == DISABLED) {
return;
}
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
final long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();//生成唯一key
retainedKeys.add(key);//retainedKeys中新增這個key
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
//包裝成WeakReference並新增到ReferenceQueue
ensureGoneAsync(watchStartNanoTime, reference);
}複製程式碼
主要就是封裝。且用retainedKeys這個set來儲存每個引用的標識key。後面將會用到。封裝完成就要開始分析了,核心方法是ensureGone:
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
removeWeaklyReachableReferences();
//看下檢測的弱引用回收了沒
if (debuggerControl.isDebuggerAttached()) {
// The debugger can create false leaks.
return RETRY;
}
if (gone(reference)) {//好,回收了,那麼這個activity沒有洩漏
return DONE;
}
gcTrigger.runGc();//還是沒有回收,手動GC一下
removeWeaklyReachableReferences();//再看看物件回收沒有
if (!gone(reference)) {
long startDumpHeap = System.nanoTime();
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = heapDumper.dumpHeap();
//竟然還沒回收,那麼懷疑是記憶體洩漏了,所以下一步dump記憶體快照.hprof下來進一步精確分析
if (heapDumpFile == RETRY_LATER) {
// Could not dump the heap.
return RETRY;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
gcDurationMs, heapDumpDurationMs));
}
return DONE;
}複製程式碼
來看removeWeaklyReachableReferences這個方法,是如何確定是否回收。
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);
}
}複製程式碼
1、如果這個物件作為弱引用,被回收了,那麼java虛擬機器會自動將他新增到引用佇列(ReferenceQueue)當中。
2、這個時候去queue.poll,將拿到弱引用ref(Activity)對應的key,retainedKeys去刪除掉這個key(之前有往裡面新增)。表明該物件的引用已經釋放。
3、再用gone(reference)來確定retainedKeys是否已經刪除對應的key了。
4、如果沒有刪除,則手動再gc一次(之前都是系統回收)。
5、再確認一下,如果還沒有回收(即retainedKeys中還有這個key),則dump下來記憶體(.hprof檔案)進行分析。
.hprof記憶體分析
通過上面的二次確認,可以確定某個object(Activity或者fragment)存在記憶體洩漏。那麼接下來就是dump下來記憶體並進行analyze。
File heapDumpFile = heapDumper.dumpHeap();
//竟然還沒回收,那麼懷疑是記憶體洩漏了,所以下一步dump記憶體快照.hprof下來進一步精確分析
if (heapDumpFile == RETRY_LATER) {
// Could not dump the heap.
return RETRY;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
heapdumpListener.analyze(//記憶體分析
new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
gcDurationMs, heapDumpDurationMs));複製程式碼
呼叫ServiceHeapDumpListener#analyze
@Override public void analyze(HeapDump heapDump) {
checkNotNull(heapDump, "heapDump");
HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
}複製程式碼
最終呼叫到HeapAnalyzerService#onHandleIntent,其中HeapAnalyzerService是獨立執行在:leakcanary程式:
@Override protected void onHandleIntent(Intent intent) {
if (intent == null) {
CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
return;
}
String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);
HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs);
AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey);
//這裡分析heapDump
AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
//這裡將分析結果傳送出去做展示
}複製程式碼
著重在HeapAnalyzer#checkForLeak
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);
//1.把.hprof轉為Snapshot,這個Snapshot物件就包含了物件引用的所有路徑
HprofParser parser = new HprofParser(buffer);
//2.精簡gcroots??
Snapshot snapshot = parser.parse();
deduplicateGcRoots(snapshot);
Instance leakingRef = findLeakingReference(referenceKey, snapshot);
//3.找出洩漏的物件
if (leakingRef == null) {
return noLeak(since(analysisStartNanoTime));
}
return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef);
//4.找出洩漏物件的最短路徑
} catch (Throwable e) {
return failure(e, since(analysisStartNanoTime));
}
}複製程式碼
上面的checkForLeak方法就是輸入.hprof,輸出分析結果,主要有以下幾個步驟:
1.把.hprof轉為Snapshot,這個Snapshot物件就包含了物件引用的所有路徑
2.精簡gcroots,把重複的路徑刪除,重新封裝成不重複的路徑的容器
3.找出洩漏的物件
4.找出洩漏物件的最短路徑
重點分析放在第3、4步:(很多處理邏輯都是haha這個第三方庫實現的)
private Instance findLeakingReference(String key, Snapshot snapshot) {
ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
List<String> keysFound = new ArrayList<>();
for (Instance instance : refClass.getInstancesList()) {
List<ClassInstance.FieldValue> values = classInstanceValues(instance);
String keyCandidate = asString(fieldValue(values, "key"));
if (keyCandidate.equals(key)) {
return fieldValue(values, "referent");
}
keysFound.add(keyCandidate);
}
throw new IllegalStateException(
"Could not find weak reference with key " + key + " in " + keysFound);
}複製程式碼
那麼上面這個方法是在snapshot快照中找到第一個弱引用(因為就是這個物件沒有回收,洩漏了),然後根據遍歷這個物件的所有例項,如果key值和最開始定義封裝的key值相同,那麼返回這個洩漏物件,就是已近在快照中定位到了洩漏物件了。
繼續看findLeakTrace
private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
Instance leakingRef) {
ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);
// False alarm, no strong reference path to GC Roots.
if (result.leakingNode == null) {
return noLeak(since(analysisStartNanoTime));
}
LeakTrace leakTrace = buildLeakTrace(result.leakingNode);
String className = leakingRef.getClassObj().getClassName();
// Side effect: computes retained size.
snapshot.computeDominators();
Instance leakingInstance = result.leakingNode.instance;
long retainedSize = leakingInstance.getTotalRetainedSize();
// TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
if (SDK_INT <= N_MR1) {
retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
}
return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
since(analysisStartNanoTime));
}複製程式碼
最後一個步驟是根據上一部得到的洩漏物件找到最短路徑,封裝在ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);
總結:在第三個分析步驟,解析hprof檔案中,是先把這個檔案封裝成snapshot,然後根據弱引用和前面定義的key值,確定洩漏的物件,最後找到最短洩漏路徑,作為結果反饋出來,那麼如果在快照中找不到這個懷疑洩漏的物件,那麼就認為這個物件其實並沒有洩漏,因為已經回收了,如下的程式碼:
if (leakingRef == null) {
return noLeak(since(analysisStartNanoTime));
}複製程式碼
總結、RefWatcher工作流程:
- RefWatcher.watch() 建立一個 KeyedWeakReference 到要被監控的物件。
- 然後在後臺執行緒檢查引用是否被清除,如果沒有,呼叫GC。
- 如果引用還是未被清除,把 heap 記憶體 dump 到 檔案系統中的一個 .hprof 檔案中。
- 在另外一個程式中,HeapAnalyzerService 通過 HeapAnalyzer 使用HAHA 解析這個檔案。
- 得益於唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位記憶體洩漏。
- HeapAnalyzer 計算 到 GC roots 的最短強引用路徑,並確定是否洩漏。如果是,建立導致洩漏的引用鏈。
- 引用鏈傳遞到 APP 程式中的 DisplayLeakService, 並以通知的形式展示出來。
以上,就是leakCanary的全部流程。