內存泄漏與Leakcanary代碼解析

本文從如下幾個問題着手,分析內存泄漏的問題,以及檢測內存泄漏的leakcanary的相關源碼解析。html

  1. 什麼是內存泄漏
  2. 什麼是JAVA垃圾回收機制
  3. 什麼操做會致使泄漏
  4. leakcanary怎麼檢測到內存泄漏的
  5. leakcanary如何找到引用鏈
  6. 如何獲取GC Root最短路徑

首先探討第一個問題:java

1、什麼是內存泄漏?

一句話歸納就是:沒有用的對象沒法回收的現象就是內存泄露node

android 系統爲每一個應用分配的內存是有限的,當一個對象已經不須要再使用了,本該被回收時,而有另一個正在使用的對象持有它的引用從而致使它不能被回收,這致使本該被回收的對象不能被回收而停留在堆內存中,這就產生了內存泄漏android

堆內存引伸:JVM內存模型算法

  1. 模型 粗淺的概念:堆與棧,實際狀況要複雜的多,下面給出一張圖來表示
    內存模型
    下面一張圖則填寫了相應區域的內容:

主要詳解:數組

1.1 虛擬機棧要點:緩存

  • 這個區域就是咱們平時說的堆棧中的棧
  • 線程私有的,與線程的聲明週期相同
  • 每一個java方法被執行的時候,這個區域都會產生一個棧幀
  • 棧幀中存放的局部變量有8種基本數據類型以及引用類型
  • java方法運行的過程就是棧幀在虛擬機棧中入棧和出棧的過程
  • 當線程請求的棧的深度超出了虛擬機容許的深度時,會拋出StackOverFlow的錯誤
  • 當Java虛擬機動態擴展到沒法申請足夠內存時會拋出OutOfMemory的錯誤

1.2 Java堆要點:安全

  • 性能優化主要針對這部份內存,GC的主要操做場所,存放全部對象實例和數組數據
  • Java堆屬於線程共享區域,全部的線程共享這一塊內存區域
  • 從內存回收角度,Java堆可被分爲新生代和老年代,這樣分可以更快的回收內存,下面會詳細介紹新生代、老年代
  • 從內存分配角度,Java堆可劃分出線程私有的分配緩存區(Thread Local Allocation Buffer,TLAB),這樣可以更快的分配內存
  • 當Java虛擬機動態擴展到沒法申請足夠內存時會拋出OutOfMemory的錯誤

1.3 方法區:性能優化

方法區主要存放的是已被虛擬機加載的類信息、常量、靜態變量、編譯器編譯後的代碼等數據。GC在該區域出現的比較少bash

1.4 運行時常量池:

運行時常量池也是方法區的一部分,用於存放編譯器生成的各類字面量和符號引用。


講到回收對象那麼就引出了第二個問題:

2、什麼是JAVA垃圾回收機制?

前面說過堆內存的特色是「進程獨立,線程共享」。換句話說,每一個JVM實例都擁有它們本身的Heap空間,從而保證了程序間的安全性,不過須要注意的是進程內部的各個線程會共享一個堆空間的狀況下的代碼同步問題。

JAVA相較其餘語言的一個重要區別就是它具有垃圾自動回收功能——這同時也是堆內存管理系統最關鍵的一個功能。隨着JVM的不斷更新換代,其所支持的垃圾回收算法也在不停地推陳出新。這裏就簡述下最流行的算法之一,即「分代垃圾回收」算法

分代垃圾回收算法

簡而言之,分代回收機制會將內存劃分爲以下「三代」來區別管理:

  • Yong Generation 即年輕代內存,又能夠分紅eden/s0/s1三種,其後2者也被稱之爲「from space」和「to space」。全部新生成的對象首先都是放在年輕代的。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象
  • Old Generation 老年代內存又被稱之爲「Tenured Space」,在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。
  • Permanent Generation 永久代內存,用於存儲和類、方法相關的Meta Data,並不屬於Heap的範疇,在某些狀況下能夠不予以考慮

工做流程:

JVM對於不一樣代中的內存鎖採用的垃圾回收算法也是有區別的。

  • 全部內存空間申請請求首先考慮從「Eden」區分配。
  • 新生代內存按照8:1:1的比例分爲一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(通常而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象複製到一個survivor0區,而後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象複製到另外一個survivor1區,而後清空eden和這個survivor0區,此時survivor0區是空的,而後將survivor0區和survivor1區交換,即保持survivor1區爲空, 如此往復。
  • 當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。如果老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收

何爲GC?

垃圾回收機制是由垃圾收集器Garbage Collection GC來實現的,GC是後臺的守護進程。它的特別之處是它是一個低優先級進程,可是能夠根據內存的使用狀況動態的調整他的優先級。所以,它是在內存中低到必定限度時纔會自動運行,從而實現對內存的回收。這就是垃圾回收的時間不肯定的緣由。

爲什麼要這樣設計:由於GC也是進程,也要消耗CPU等資源,若是GC執行過於頻繁會對java的程序的執行產生較大的影響(java解釋器原本就不快),所以JVM的設計者們選着了不按期的gc。

GC的根節點是什麼?

每一個應用程序都包含一組根(root)。每一個根都是一個存儲位置,其中包含指向引用類型對象的一個指針。該指針要麼引用託管堆中的一個對象,要麼爲null。

在應用程序中,只要某對象變得不可達,也就是沒有根(root)引用該對象,這個對象就會成爲垃圾回收器的目標。


接下來看看哪些操做會形成內存泄漏,以及處理辦法:

3、什麼操做會致使泄漏

能夠簡單的作以下歸類:

  1. 長期持有(Activity)Context致使
  2. 忘記註銷監聽器或者觀察者
  3. 由非靜態內部類致使的

例子

  1. 長時間持有Activity實例

好比咱們有一個叫作AppSettings的類,它是一個單例模式

public class AppSettings {
    private Context mAppContext;
    private static AppSettings sInstance = new AppSettings();

    //some other codes
    public static AppSettings getInstance() {
        return sInstance;
    }

    public final void setup(Context context) {
        mAppContext = context;
    }
}
複製代碼

當咱們傳入Activity做爲Context參數時,AppSettings實例會持有這個Activity的實例。 又當咱們旋轉設備時,Android系統會銷燬當前的Activity,建立新的Activity來加載合適的佈局。若是出現Activity被單例實例持有,那麼旋轉過程當中的舊Activity沒法被銷燬掉。就發生了內存泄漏。

解決辦法: 那就是使用Application的Context對象,由於它和AppSettings實例具備相同的生命週期。這裏是經過使用Context.getApplicationContext()方法來實現。

  1. 忘記反註冊監聽器

在Android中咱們會使用不少listener,observer。這些都是做爲觀察者模式的實現。當咱們註冊一個listener時,這個listener的實例會被Activity所引用。若是listener的生命週期要明顯大於Activity,那麼就有可能發生內存泄漏。

public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        NetworkManager.getInstance().registerListener(this);
    }

    @Override
    public void onNetworkUp() {

    }

    @Override
    public void onNetworkDown() {

    }
}
複製代碼

解決辦法: 在Activity onDestroy()時調用unregisterListener方法,解綁。

  1. 非靜態內部類致使的內存泄漏

見下面的代碼:

public class MainActivity extends AppCompatActivity
{
	private static Leak leak;
	@Override
	protected void onCreate( @Nullable Bundle savedInstanceState )
	{
		super.onCreate( savedInstanceState );
		setContentView( R.layout.activity_main );
		leak = new Leak();
	}
	
	private class Leak{
	
	}
}
複製代碼

非靜態內部類會默認持有外部類的引用,當MainActivity銷燬重建後因爲其內部類Leak持有了它的引用,而且Leak是靜態的,生命週期和應用同樣長,所以致使LeakActivity沒法被銷燬,所以一直存在於內存中。 要銷燬MainActivity,必須先銷燬leak,可是要銷燬mLeak,必須先銷燬LeakActivity,因此一個也不能銷燬。就形成了內存泄漏。

經過反編譯後就能清楚的看出來了

解決辦法: 1.及時銷燬 2.放到Application中


下面主要分析下內存泄漏檢測工具leakcanary

4、leakcanary怎麼檢測到內存泄漏的

引入方法,只需下面兩步:

debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'

public class StudyApplication extends Application
{
	@Override
	public void onCreate()
	{
		super.onCreate();
		if( LeakCanary.isInAnalyzerProcess( this ) )
		{
			return;
		}
		LeakCanary.install( this );
	}
}
複製代碼

LeakCanary.install( this );開始

/**
   * Creates a {@link RefWatcher} that works out of the box, and starts watching activity
   * references (on ICS+).
   */
  public static @NonNull RefWatcher install(@NonNull Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
        .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
        .buildAndInstall();
  }
複製代碼
  • 第一步refWatcher(application)建立AndroidRefWatcherBuilder對象
  • 第二步listenerServiceClass(DisplayLeakService.class)建立解析內存泄漏信息的服務,這裏也能夠傳遞繼承AbstractAnalysisResultService的自定義對象,用於上傳內存泄漏信息
  • 第三步excludedRefs(AndroidExcludedRefs.createAppDefaults().build())設置過濾,過濾掉安卓系統自己出現的內存泄漏現象,只保留用戶app出現的內存泄漏
  • 第四步buildAndInstall()構造RefWatcher對象,並返回
public @NonNull RefWatcher buildAndInstall() {
    ...
    RefWatcher refWatcher = build();
    if (refWatcher != DISABLED) {
      ...
      if (watchActivities) {
        ActivityRefWatcher.install(context, refWatcher);
      }
      if (watchFragments) {
        FragmentRefWatcher.Helper.install(context, refWatcher);
      }
    }
    ...
    return refWatcher;
  }
複製代碼

這裏有2個重要的操做build();,ActivityRefWatcher.install(context, refWatcher);第一個方法用於生成RefWatcher 它是用來監控引用的工具,第二個方法,建立了ActivityRefWatcher,這個就是用來監控Activity泄漏情況的其中:

public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);

    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
  }
複製代碼

監控的遠離就是在application中註冊了一個ActivitylifecycleCallbacks的回調函數,用來監聽Application整個生命週期中全部Activity的lifecycle事件。而這個lifecycleCallbacks,就是

private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
    new ActivityLifecycleCallbacksAdapter() {
    @Override public void onActivityDestroyed(Activity activity) {
      refWatcher.watch(activity);
    }
};
public abstract class ActivityLifecycleCallbacksAdapter
    implements 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) {
  }
}
複製代碼

它只監聽了全部Activity的onActivityDestroyed事件,當Activity被destroy時,調用refWatcher.watch(activity);函數,將目標activity對象傳遞到RefWatcher,讓它去監控這個activity是否被回收了,若是沒有被回收,則發生了內存泄漏事件。

深刻refWatcher.watch(activity)方法

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();
    retainedKeys.add(key);
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    ensureGoneAsync(watchStartNanoTime, reference);
  }
複製代碼

checkNotNull(watchedReference, "watchedReference")判斷watchedReference是否爲空,若是爲空,就不須要繼續進行下去了,接下來是構造了一個惟一key,並傳入retainedKeys中,咱們想要觀測的activity對應的惟一key都會存放到集合裏面,以後把咱們傳入的activity包裝成一個KeyedWeakReference(能夠看成WeakReference),而後執行ensureGoneAsync這個方法最後會執行一個Runnable,調用ensureGone(reference, watchStartNanoTime);

咱們知道watch函數自己就是用來監聽activity是否被回收掉了,這就涉及到兩個問題:

  1. 什麼時候檢查它是否回收?
  2. 如何有效檢查它真的被回收了

對於這個ensureGone(reference, watchStartNanoTime);函數它作的事情就是確保reference被回收掉了,不然就意味着內存泄漏

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }
複製代碼

這個watchExecutor在LeakCanary中是AndroidWatchExecutor的實例,調用它的execute方法實際上就是向主線程的消息隊列中插入了一個IdleHandler消息,這個消息只有在對應的消息隊列爲空的時候纔會去執行,所以RefWatcher的watch方法就保證了在主線程空閒的時候纔會去執行ensureGone方法,防止由於內存泄漏檢查任務而嚴重影響應用的正常執行.

在說下面以前,先解釋下WeakReferenceReferenceQueue的工做原理

  1. 弱引用WeakReference 被強引用的對象就算髮生 OOM 也永遠不會被垃圾回收機回收;被弱引用的對象,只要被垃圾回收器發現就會當即被回收;被軟引用的對象,具有內存敏感性,只有內存不足時纔會被回收,經常使用來作內存敏感緩存器;虛引用則任意時刻均可能被回收,使用較少。

  2. 引用隊列 ReferenceQueue 咱們經常使用一個 WeakReference reference = new WeakReference(activity);,這裏咱們建立了一個 reference 來弱引用到某個 activity,當這個 activity 被垃圾回收器回收後,這個 reference 會被放入內部的 ReferenceQueue 中。也就是說,從隊列 ReferenceQueue 取出來的全部 reference,它們指向的真實對象都已經成功被回收了。

探究ensureGone(reference, watchStartNanoTime)檢測回收方法

先看實現的代碼:

@SuppressWarnings("ReferenceEquality") // Explicitly checking for named null.
  Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    removeWeaklyReachableReferences();
    
    // 若是正在debug斷點調試,則延遲執行檢查(由於斷點會影響準確性)
    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    
    if (gone(reference)) {
      return DONE;
    }
    // 若是沒有被回收,則觸發一次GC
    gcTrigger.runGc();
    // 再次將已回收的對象對應的key從retainedKeys中移除
    removeWeaklyReachableReferences();
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
        
      // 獲取堆內存鏡像文件
      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

      HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();

      heapdumpListener.analyze(heapDump);
    }
    return DONE;
  }
複製代碼

基於咱們對ReferenceQueue的瞭解,只要把隊列中全部的reference取出來,並把對應的retainedKeys 裏的 key 移除,剩下的 key 對應的對象都沒有被回收。

具體步驟就是:

  1. 調用removeWeaklyReachableReferences();把已被回收的對象key從retainedKeys中移除,剩下的key都是未被回收的對象
  2. if (gone(reference))來判斷某個reference的key是否還在retainedKey集合中,若不在,表示以及被回收了,不然繼續
  3. gcTrigger.runGc();手動觸發gc,當即把全部WeakReference 引用的對象回收
  4. removeWeaklyReachableReferences();再次清理retainedKeys,若是該 reference 還在 retainedKeys 裏(if (!gone(reference))),表示泄漏
  5. 調用heapDumper.dumpHeap()將內存狀況dump成文件,併發送Notification,以及Toast
  6. 將文件以及reference.key等其餘信息封裝成HeapDump對象,並調用heapdumpListener.analyze(heapDump);進行分析

至此Leakcanary檢測到內存泄漏的流程就看完了。

小結

  1. 利用 application.registerActivityLifecycleCallbacks(lifecycleCallbacks) 來監聽整個生命週期內的 Activity onDestoryed 事件
  2. 某個 Activity 被 destory 後,將它傳給 RefWatcher 去作觀測,確保其後續會被正常回收;
  3. RefWatcher 首先把 Activity 使用 KeyedWeakReference 引用起來,並使用一個 ReferenceQueue 來記錄該 KeyedWeakReference 指向的對象是否已被回收;
  4. AndroidWatchExecutor 會在 5s 後,開始檢查這個弱引用內的 Activity 是否被正常回收。判斷條件是:若 Activity 被正常回收,那麼引用它的 KeyedWeakReference 會被自動放入 ReferenceQueue 中。
  5. 判斷方式是:先看 Activity 對應的 KeyedWeakReference 是否已經放入 ReferenceQueue 中;若是沒有,則手動 GC:gcTrigger.runGc();;而後再一次判斷 ReferenceQueue 是否已經含有對應的 KeyedWeakReference。若還未被回收,則認爲可能發生內存泄漏。

5、leakcanary如何找到引用鏈

如今來主要看下調用heapdumpListener.analyze(heapDump);後進行的分析

@Override public void analyze(@NonNull HeapDump heapDump) {
    checkNotNull(heapDump, "heapDump");
    HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
  }
複製代碼

這個方法最後調用HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);進行分析,傳遞的參數context,heapDump,以及分析完畢後的回調類。HeapAnalyzerService繼承自ForegroundService用於分析,並運行在另外一個獨立進程中,而runAnalysis方法最後調用ContextCompat.startForegroundService(context, intent);來啓動這個service

在service啓動後調用onHandleIntentInForeground方法

@Override protected void onHandleIntentInForeground(@Nullable 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, this, heapDump.reachabilityInspectorClasses);

    AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey,
        heapDump.computeRetainedHeapSize);
    AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
  }
複製代碼

這裏分析內存的主要操做在HeapAnalyzer當中,分析完成後獲取內存泄漏點以及引用鏈 AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);獲取結果後,進行回調,回調給以前設置的listenerClassName的那個類,也能夠是開發者本身繼承的自定義類。

深刻heapAnalyzer.checkForLeak()方法進行內存分析

public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
      @NonNull String referenceKey,
      boolean computeRetainedSize) {
   ...
    try {
      listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
      HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
      HprofParser parser = new HprofParser(buffer);
      listener.onProgressUpdate(PARSING_HEAP_DUMP);
      Snapshot snapshot = parser.parse();
      listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
      // 從內存鏡像中獲取全部的GC Roots,並將它們添加到一個集合中
      deduplicateGcRoots(snapshot);
      listener.onProgressUpdate(FINDING_LEAKING_REF);
       // 使用反射,經過key找到泄露的對象實例
      Instance leakingRef = findLeakingReference(referenceKey, snapshot);

      // False alarm, weak reference was cleared in between key check and heap dump.
      if (leakingRef == null) {
        String className = leakingRef.getClassObj().getClassName();
        return noLeak(className, since(analysisStartNanoTime));
      }
      //查找泄露路徑
      return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
    } catch (Throwable e) {
      return failure(e, since(analysisStartNanoTime));
    }
  }
複製代碼

這個方法第一步就是利用Haha庫將以前dump出來的內存文件解析成Snapshot對象,其中調用到的方法包括SnapshotFactory的parse和HprofIndexBuilder的fill方法。。解析獲得的Snapshot對象直觀上和咱們使用MAT進行內存分析時候羅列出內存中各個對象的結構很類似,它經過對象之間的引用鏈關係構成了一棵樹,咱們能夠在這個樹種查詢到各個對象的信息,包括它的Class對象信息、內存地址、持有的引用及被持有的引用關係等。

以後LeakCanary就須要在Snapshot中找到一條有效的到被泄漏對象之間的引用路徑。首先它調用findLeakingReference方法來從Snapshot中找到被泄漏對象 重要的方法有2個

  1. findLeakingReference(referenceKey, snapshot);獲取內存泄漏實例
  2. findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);分析對應的引用鏈

先來看第一個findLeakingReference

private Instance findLeakingReference(String key, Snapshot snapshot) {
    ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
    if (refClass == null) {
      throw new IllegalStateException(
          "Could not find the " + KeyedWeakReference.class.getName() + " class in the heap dump.");
    }
    List<String> keysFound = new ArrayList<>();
    for (Instance instance : refClass.getInstancesList()) {
      List<ClassInstance.FieldValue> values = classInstanceValues(instance);
      Object keyFieldValue = fieldValue(values, "key");
      if (keyFieldValue == null) {
        keysFound.add(null);
        continue;
      }
      String keyCandidate = asString(keyFieldValue);
      if (keyCandidate.equals(key)) {//匹配key
        return fieldValue(values, "referent");//定位泄漏對象
      }
      keysFound.add(keyCandidate);
    }
    throw new IllegalStateException(
        "Could not find weak reference with key " + key + " in " + keysFound);
  }
複製代碼

爲了可以準確找到被泄漏對象,LeakCanary經過被泄漏對象的弱引用來在Snapshot中定位它。由於,若是一個對象被泄漏,必定也能夠在內存中找到這個對象的弱引用,再經過弱引用對象的referent就能夠直接定位被泄漏對象。


6、如何獲取GC Root最短路徑

上面的方法實現了內存泄漏的實例查找。下一步的工做就是找到一條有效的到被泄漏對象的最短的引用,這經過findLeakTrace來實現,其代碼以下

private AnalysisResult findLeakTrace(long analysisStartNanoTime, Snapshot snapshot,
      Instance leakingRef, boolean computeRetainedSize) {

    listener.onProgressUpdate(FINDING_SHORTEST_PATH);
    ShortestPathFinder pathFinder = new ShortestPathFinder(excludedRefs);
    ShortestPathFinder.Result result = pathFinder.findPath(snapshot, leakingRef);
    
    String className = leakingRef.getClassObj().getClassName();

    // False alarm, no strong reference path to GC Roots.
    if (result.leakingNode == null) {
      return noLeak(className, since(analysisStartNanoTime));
    }

    listener.onProgressUpdate(BUILDING_LEAK_TRACE);
    // 根據查找的結果,創建泄露路徑
    LeakTrace leakTrace = buildLeakTrace(result.leakingNode);

    long retainedSize;
    if (computeRetainedSize) {

      listener.onProgressUpdate(COMPUTING_DOMINATORS);
      // Side effect: computes retained size.
      snapshot.computeDominators();

      Instance leakingInstance = result.leakingNode.instance;

      retainedSize = leakingInstance.getTotalRetainedSize();

      // TODO: check O sources and see what happened to android.graphics.Bitmap.mBuffer
      if (SDK_INT <= N_MR1) {
        listener.onProgressUpdate(COMPUTING_BITMAP_SIZE);
        retainedSize += computeIgnoredBitmapRetainedSize(snapshot, leakingInstance);
      }
    } else {
      retainedSize = AnalysisResult.RETAINED_HEAP_SKIPPED;
    }

    return leakDetected(result.excludingKnownLeaks, className, leakTrace, retainedSize,
        since(analysisStartNanoTime));
  }
複製代碼

此方法中pathFinder.findPath(snapshot, leakingRef);用於獲取最短GC ROOT路徑,第一個參數是帶有全部信息的snapshot對象,第二個參數是內存泄漏的那個類的封裝對象,使用算法廣度優先搜索法

findLeakTrace 方法整體的邏輯就是

  1. 創建內存泄漏點到 GC Roots 的最短引用鏈
  2. 計算整個內存泄漏的大小 retained size
  3. 構建 LeakTrace
  4. 構建 AnalysisResult 具體代碼以下:
Result findPath(Snapshot snapshot, Instance leakingRef) {
    clearState();
    canIgnoreStrings = !isString(leakingRef);
    // 將上面找到的全部GC Roots添加到隊列中
    enqueueGcRoots(snapshot);

    boolean excludingKnownLeaks = false;
    LeakNode leakingNode = null;
    // 若是將從GC Root開始的全部引用看作樹,則這裏就能夠理解成使用廣度優先搜索遍歷引用「森林」
    // 若是將全部的引用都看作是長度爲1的Edge,那麼這些引用就組成了一幅有向圖,
    // 這裏就是使用相似Dijkstra算法的方法來尋找最短路徑,越在隊列後面的,距離GC Roots越遠
    while (!toVisitQueue.isEmpty() || !toVisitIfNoPathQueue.isEmpty()) {
      LeakNode node;
        // 若是toVisitQueue中沒有元素,則取toVisitIfNoPathQueue中的元素
        // 意思就是,若是遍歷完了toVisitQueue尚未找到泄露的路徑,那麼就繼續遍歷設置了「例外」的那些對象
        // 「例外」是什麼狀況?在後續兩個方法會講到。
      if (!toVisitQueue.isEmpty()) {
        node = toVisitQueue.poll();
      } else {
        node = toVisitIfNoPathQueue.poll();
        if (node.exclusion == null) {
          throw new IllegalStateException("Expected node to have an exclusion " + node);
        }
        excludingKnownLeaks = true;
      }

      // Termination
      if (node.instance == leakingRef) {
        leakingNode = node;
        break;
      }

      // 由於一個對象能夠被多個對象引用,以GC Root爲根的引用樹
      // 並非嚴格意義上的樹,因此若是已經遍歷過當前對象,就跳過
      if (checkSeen(node)) {
        continue;
      }
      // 下面是根據當前引用節點的類型,分別找到它們所引用的對象
      if (node.instance instanceof RootObj) {
        visitRootObj(node);
      } else if (node.instance instanceof ClassObj) {
        visitClassObj(node);
      } else if (node.instance instanceof ClassInstance) {
        visitClassInstance(node);
      } else if (node.instance instanceof ArrayInstance) {
        visitArrayInstance(node);
      } else {
        throw new IllegalStateException("Unexpected type for " + node.instance);
      }
    }
    // 返回查找結果
    return new Result(leakingNode, excludingKnownLeaks);
  }
複製代碼

其中enqueueGcRoots(snapshot);會遍歷獲取全部的GCROOT並放入搜索隊列中 while (!toVisitQueue.isEmpty() || !toVisitIfNoPathQueue.isEmpty())循環條件優先找toVisitQueue隊列,找完在找toVisitIfNoPathQueue隊列,而路徑中包含toVisitIfNoPathQueue裏的元素則標示excludingKnownLeaks爲true

爲了說明excludingKnownLeaks,以visitClassInstance(node);代碼爲例

private void visitClassInstance(LeakNode node) {
  ClassInstance classInstance = (ClassInstance) node.instance;
  Map<String, Exclusion> ignoredFields = new LinkedHashMap<>();
  ClassObj superClassObj = classInstance.getClassObj();
  Exclusion classExclusion = null;
  // 將設置了「例外」的對象記錄下來
  // 這裏的「例外」就是上一個方法中提到的「例外」。是指那些低優先級的,或者說幾乎不可能引起內存泄露的對象
  // 例如SDK中的一些對象,諸如Message, InputMethodManager等,通常狀況下,這些對象都不會致使內存泄露。
  // 所以只有在遍歷了其餘對象以後,找不到泄露路徑的狀況下,才遍歷這些對象。
  while (superClassObj != null) {
    Exclusion params = excludedRefs.classNames.get(superClassObj.getClassName());
    if (params != null) {
      // true overrides null or false.
      // 若是當前類或者其父類被設置了「例外」,則將其賦值給classExclusion
      if (classExclusion == null || !classExclusion.alwaysExclude) {
        classExclusion = params;
      }
    }

    // 若是當前類及其父類包含例外的成員,將這些成員添加到ignoredFields中
    Map<String, Exclusion> classIgnoredFields =
        excludedRefs.fieldNameByClassName.get(superClassObj.getClassName());
    if (classIgnoredFields != null) {
      ignoredFields.putAll(classIgnoredFields);
    }
    superClassObj = superClassObj.getSuperClassObj();
  }

  if (classExclusion != null && classExclusion.alwaysExclude) {
    return;
  }

  // 遍歷每個成員
  for (ClassInstance.FieldValue fieldValue : classInstance.getValues()) {
    Exclusion fieldExclusion = classExclusion;
    Field field = fieldValue.getField();
    // 若是成員不是對象,則忽略
    if (field.getType() != Type.OBJECT) {
      continue;
    }
    // 獲取成員實例
    Instance child = (Instance) fieldValue.getValue();
    String fieldName = field.getName();
    Exclusion params = ignoredFields.get(fieldName);
    // 若是當前成員對象是例外的,而且當前類和全部父類都不是例外的(classExclusion = null),
    // 或,若是當前成員對象時例外的,並且是alwaysExclude,並且當前類和父類都不是alwaysExclude
    // 則認爲當前成員是須要例外處理的。
    // 這個邏輯很繞,實際上「||」後面的判斷是不須要的,具體在enqueue方法中講。
    if (params != null && (fieldExclusion == null || (params.alwaysExclude
        && !fieldExclusion.alwaysExclude))) {
      fieldExclusion = params;
    }

    // 入隊列
    enqueue(fieldExclusion, node, child, fieldName, INSTANCE_FIELD);
  }
}
複製代碼

再看下enqueue()方法

private void enqueue(Exclusion exclusion, LeakNode parent, Instance child, String referenceName,
    LeakTraceElement.Type referenceType) {
  if (child == null) {
    return;
  }
  if (isPrimitiveOrWrapperArray(child) || isPrimitiveWrapper(child)) {
    return;
  }
  // Whether we want to visit now or later, we should skip if this is already to visit.
  if (toVisitSet.contains(child)) {
    return;
  }

  // 這個exclusion就是上一個方法經過「很繞的」邏輯判斷的出來的
  // 這裏的做用就是若是爲null則visitNow,這個boolean值在下面會用到。
  // 能夠看到這裏只是判斷exclusion是否爲null,並無使用到alwaysExclude參數,
  // 因此說上一個方法中,「||」以後的判斷是沒有必要的。
  boolean visitNow = exclusion == null;
  if (!visitNow && toVisitIfNoPathSet.contains(child)) {
    return;
  }
  if (canIgnoreStrings && isString(child)) {
    return;
  }
  if (visitedSet.contains(child)) {
    return;
  }
  LeakNode childNode = new LeakNode(exclusion, child, parent, referenceName, referenceType);
  // 這裏用到了boolean值visitNow,就是說若是exclusion對象爲null,則表示這不是一個例外的對象(暫且稱之爲常規對象);
  // 若是exclusion對象不爲null,則表示這個對象是例外對象,只有在遍歷全部常規對象以後,仍是找不到路徑的狀況下才會被遍歷。
  if (visitNow) {
    toVisitSet.add(child);
    toVisitQueue.add(childNode);
  } else {
    toVisitIfNoPathSet.add(child);
    toVisitIfNoPathQueue.add(childNode);
  }
}
複製代碼

查找最短強引用路徑的流程以下圖:

下面附一張leakcanary的流程圖


7、leakcanary中Shark的代碼分析

Shark是Leakcanary 2.0.0時推出的Heap分析工具,替代了以前使用的HAHA庫,其做者稱它比haha使用的perflib快6倍,使用的內存倒是以前的10分之一 Shark文件架構以下:

  1. shark:生成heap分析報告
  2. shark-android:生成安卓的heap分析報告
  3. share-cli:在電腦上分析手機裏的安卓app,沒必要依賴leakcanary到項目中去
  4. shark-graph:heap對象的導航圖
  5. shark-hprof:讀寫heap文件記錄 使用方法:
dependencies {
  implementation 'com.squareup.leakcanary:shark-android:$sharkVersion'
}
複製代碼
相關文章
相關標籤/搜索