1. 程式人生 > >用 LeakCanary 檢測記憶體洩漏

用 LeakCanary 檢測記憶體洩漏

轉載自:https://academy.realm.io/cn/posts/droidcon-ricau-memory-leaks-leakcanary/


我們的 App 曾經遇到很多的記憶體洩漏導致 OutOfMemoryError 的崩潰,一些甚至是在生產環境。Square 的 Pierre-Yvews Ricau 開發了 LeakCanary 最終解決了這些問題。LeakCanary 是一個幫助你檢測和修復記憶體洩漏的工具。在這個分享中,Pierre 教授大家如何修復記憶體洩漏的錯誤,讓你的 App 更穩定和可靠。


Save the date for Droidcon SF in March — a conference with best-in-class presentations from leaders in all parts of the Android ecosystem.


介紹 (0:00)

Square 出了一款名為:Square Register 的 App, 幫助你用移動裝置完成支付。在用這個 App 的時候,使用者先要登陸他的個人賬號。

不幸的是,在簽名頁面有的時候會因為記憶體溢位而出現崩潰。老實說,這個崩潰來的太不是時候了 — 使用者和商家都無法確認交易是否完成了,更何況是在和錢打交道的時候。我們也強烈的意識到,我們需要處理下記憶體溢位或者記憶體洩露這種事情了。

記憶體洩漏:非技術講解 (1:40)

我想要聊的記憶體洩露解決方案是: LeakCanary。 LeakCanary 是一個可以幫助你發現和解決記憶體洩露的開源工具。但是到底什麼是記憶體洩露呢?我們從一個非技術角度來開始,先來舉個例子。

假設我的手代表著我們 App 能用的所有記憶體。我的手裡能放很多東西。比如:鑰匙,Android 玩偶等等。設想我的 Android 玩偶需要揚聲器才能工作,而我的揚聲器也需要依賴 Android 玩偶才能工作,因此他們持有彼此的引用。

我的手裡可以持有如此多的東西。揚聲器依附到 Android 玩偶上會增加總重量,就像引用會佔用記憶體一樣。一旦我放棄了我的玩偶,把他扔到地上,會有垃圾回收器來回收掉它。一旦所有的東西都進了垃圾桶,我的手又輕便了。

不幸的是,有的時候,一些不好的情況會發生。比如:我的鑰匙沒準和我的 Android 玩偶黏在了一起,阻止我把 Android 玩偶扔到地上。最終的結果就是 Android 玩偶無論如何都不會被回收掉。這就是記憶體洩露

有外部的引用(鑰匙,揚聲器)指向了 本不應該再指向的物件(Andorid 玩偶)。類似這樣的小規模的記憶體洩露堆積以後就會造成大麻煩。

LeakCanary to the Rescue (3:47)

這就是我們為什麼要開發 LeakCanary。

我現在可能已經清楚了 可被回收的 Android 物件應該及時被銷燬。

但是我還是沒法清楚餓看到這些物件是否已經被回收掉。有了 LeakCanary 以後,我們給可被回收的 Android 物件上打了智慧標記。智慧標記能知道他們所指向的物件是否被成功釋放掉。如果過一小段時間物件依然沒有被釋放,他就會給記憶體做個快照。

LeakCanary 隨後會把結果釋出出來,幫助我們看到記憶體到底怎麼洩露了,清晰的展示無法被釋放的物件的引用鏈。

舉個具體的例子:在我們的 Square App 裡的簽名頁面。使用者準備簽名的時候,App 因為記憶體溢位出錯崩潰了。我們不能確認記憶體錯誤到底出在哪兒了。

簽名頁面持有了一個很大的有使用者簽名的 Bitmap 圖片物件。圖片的大小和使用者手機螢幕大小一致 — 我們猜測這個有可能會造成記憶體洩露。首先,我們可以配置 Bitmap 為 alpha 8-bit 來節省記憶體。這是很常見的一種修復方案,而且效果也不錯。但是並沒有徹底解決問題,只是減少了洩露的記憶體總量。但是記憶體洩露依然在哪兒。

最主要的問題是我們 App 的堆滿了,應該要留有足夠的空間給我們的簽名圖片,但是由於很多處的記憶體洩露疊加在一起佔用了很多記憶體。

技術講解記憶體洩漏 (8:06)

假設,我有一個 App,這個 App 點一下就能買一個法棍麵包(哈哈,可能只有法國人需要這樣的App,沒錯,我就是法國人)。

private static Button buyNowButton;

由於某種原因,我把這個 button 設定成了 static 的。問題隨之而來,這個按鈕除非你設定成了null,不然就記憶體洩露了。

你也許會說:“只是一個按鈕而已,沒啥大不了”。問題是這個按鈕還有一個成員變數:叫 “mContext”,這個東西指向了一個 Acitvity,Acitivty 又指向了一個 Window,Window 有擁有整個 View 繼承樹。算下來,那可是一大段的記憶體空間。

靜態的變數是 GC root 型別的一種。垃圾回收器會嘗試回收所有非 GC root 的物件,或者某些被 GC root 持有的物件。所以如果你建立一個物件,並且移除了這個物件的所有指向,他就會被回收掉。但是一旦你將一個物件設定成 GC root,那他就不會被回收掉。

當你看到類似“法棍按鈕”的時候,很顯然這個按鈕持有了一個 Activity 的引用,所以我們必須清理掉它。當你沉浸在你的程式碼的時候,你肯定很難發現這個問題。你可能只看到了引出的引用。你可以知道 Activity 引用了一個 Window,但是誰引用了 Activity?

Receive news and updates from Realm straight to your inbox

訂閱 Comments 

你可以用像 IntelliJ 這樣的工具做些分析,但是它並不會告訴你所有的東西。通常,你可以把這些 Object 的引用關係組織成圖,但是是個單向圖。

分析堆 (10:16)

我們能做些什麼呢?我們來做個快照。我們拿出所有的記憶體然後匯出到檔案裡,這個檔案會被用來分析和解析堆結構。其中一個工具叫做 Memory Analyzer,也叫 MAT。它會通過 dump 的記憶體,然後分析所有存活在記憶體中的物件和類。你可以用 SQL 對他做些查詢,類似如下:

SELECT * FROM INSTANCEOF android.app.Activity a WHERE a.mDestroyed = true

這條語句會返回所有的狀態為 destroyed 的例項。一旦你發現了洩露的 Activity,你可以執行 merge_shortest_paths 的操作來計算出最短的 GC root 路徑。從而找出阻止你 Acitivty 釋放的那個物件。

之所以說要 “最短路徑”,是因為通常從一個 GC root 到 Acitivty,有很多條路徑可以到達。比如說:我的按鈕的 parent view,同樣也持有一個 mContext 物件。

當我們看到記憶體洩露的時候,我們通常不需要去檢視所有的這些路徑。我們只需要最短的一條。那樣的話,我們就排除了噪音,很快的找到問題所在。

LeakCanary 救你於水火之中 (12:04)

有 MAT 這樣一個幫我們發現記憶體洩露的工具是個很棒的事情。但是在一個正在執行的 App 的上下文中,我們很難像我們的使用者發現洩露那樣發現問題所在。我們不能要求他們在做一遍相同操作,然後留言描述,再把 70多 MB 的檔案發回給我們。我們可以在後臺做這個,但是並不 Cool。我們期望的是,我們能夠儘早的發現洩露。比如在我們開發的時候就發現這些問題。這也是 LeakCanary 誕生的意義。

一個 Activity 有自己生命週期。你瞭解它是如何被建立的,如何被銷燬的,你期望他會在 onDestroy() 函式呼叫後,回收掉你所有的空閒記憶體。如果你有一個能夠檢測一個物件是否被正常的回收掉了的工具,那麼你就會很驚訝的喊出:“這個可能造成記憶體洩露!仍然沒有被垃圾回收掉,它本該被回收掉的!”

Activity 無處不在。很多人都把 Activity 當做神級 Object 一般的存在,因為它可以操作 Services,檔案系統等等。經常會發生物件洩漏的情況,如果洩漏物件還持有 context 物件,那 context 也就跟著洩漏了。

public class MyActivity extends Activity {

  @Override protected void onDestroy() {
    super.onDestroy();
    // instance should be GCed soon.
  }
}
Resources resources = context.getResources();
LayoutInflater inflater = LayoutInflater.from(context);
File filesDir = context.getFilesDir();
InputMethodManager inputMethodManager =
  (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);

LeakCanary API Walkthrough (13:32)

我們回過頭來再看看智慧標記(smart pin),我們希望知道的是當生命後期結束後,發生了什麼。幸運的時,LearkCanary有一個很簡單的 API。

第一步:建立 RefWatcher。給 RefWatcher 傳入一個物件的例項,它會檢測這個物件是否被成功釋放掉。

public class ExampleApplication extends Application {

  public static RefWatcher getRefWatcher(Context context) {
    ExampleApplication application = (exampleApplication) context.getApplicationContest();
    return application.refWatcher;
  }

  private RefWatcher refWatcher;

  @Override public void onCreate () {
    super.onCreate();
    // Using LeakCanary
    refWatcher = LeakCanary.install(this);
  }
}

第���步:監聽 Activity 生命週期。然後,當 onDestroy 被呼叫的時候,我們傳入 Activity。

public ActivityRefWatcher(Application application, final RefWatcher refWatcher) {
  this.application = checkNotNull(application, "application");
  checkNotNull(refWatcher, "androidLeakWatcher");
  lifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() {
    @Override public void onActivityDestroyed(Activity activity) {
      refWatcher.watch(activity);
    }
  };
}

public void watchActivities() {
  // Make sure you don’t get installed twice.
  stopWatchingActivities();
  application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
}

What are Weak References (14:17)

想要了解這個是怎麼工作的,我得先跟大家聊聊弱引用(weak reference)。我剛才提到過靜態域的變數會持有Activity 的引用。所以剛才說的“下單”按鈕就會持有 mContext 物件,導致 Activity 無法被釋放掉。這個被稱作強引用(strong reference)。在垃圾回收過程中,你可以對一個物件有很多的強引用。當這些強引用的個數總和為零的時候,垃圾回收器就會釋放掉它。

弱引用,就是一種不增加引用總數的持有引用方式。垃圾回收期是否決定要回收一個物件,只取決於它是否還存在強引用。所以說,如果我們將我們的 Activity 持有為弱引用,一旦我們發現弱引用持有的物件已經被銷燬了,那麼這個 Activity 就已經被垃圾回收器回收了。否則,那可以大概確定這個 Activity 已經被洩露了。

private static Button buyNowButton;

Context mContext;
WeakReference<T>

/** Treated specially by GC. */
T referent;
public class Baguette Activity
  extends Activity {

  @Override protected void onCreate(Bundle state) {
    super.onCreate(state);
    setContentView(R.layout.activity_main);
  }
}

弱引用的主要目的是為了做 Cache,而且非常有用。主要就是告訴 GC,儘管我持有了這個物件,但是如果一旦沒有物件在用這個物件的時候,GC 就可以在需要的時候銷燬掉。

在下面的例子中,我們繼承了 WeakReference:

final class KeyedWeakReference extends WeakReference<Object> {
  public final String key; // (1) Unique identifier
  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");
  }
}

你可以看到,我們給弱引用添加了一個 Key,這個 Key 是一個唯一字串。想法是這樣的:當我們解析一個 heap dump 檔案的時候,我們可以詢問所有的 KeyedWeakReference 例項,然後找到對應的 Key。

首先,我們建立一個 weakReference,然後我們寫入『一會兒,我需要檢查弱引用』。(儘管一會兒可能就是幾秒後)。當我們呼叫 watch 函式的時候,其實就是發生了這些事情。

public void watch(Object watchedReference, String referenceName) {
  checkNotNull(watchedReference, "watchedReference");
  checkNotNull(referenceName, "referenceName");
  if (debuggerControl.isDebuggerAttached()) {
    return;
  }
  final long watchStartNanoTime = System.nanoTime();
  String key = UUID.randomUUID().toString();
  retainedKeys.add(key);
  final KeyedWeakReference reference =
    new KeyedWeakReference(watchedReference, key, referenceName, queue);

  watchExecutor.execute(()  { ensureGone(reference, watchStartNanoTime); });
}

在這一切的背後,我們呼叫了 System.GC — 免責宣告— 我們本不應該去做這件事情。然而,這是一種告訴垃圾回收器:『Hey,垃圾回收器,現在是一個不錯的清理垃圾的時機。』,然後我們再檢查一遍,如果發現有些物件依然存活著,那麼可能就有問題了。我們就要觸發 heap dump 操作了。

HAHA! (16:55)

親手做 heap dump 是件超酷的事情。當我親手做這些的時候,花了很多時間和功夫。我每次都是做相同的操作:下載 heap dump 檔案,在記憶體分析工具裡開啟它,找到例項,然後計算最短路徑。但是我很懶,我根本不想一次次的做這個。(我們都很懶對吧,因為我們是開發者啊!)

我本可以為記憶體分析器寫一個 Eclipse 外掛,但是 Eclipse 外掛機制太糟糕了。 後來我靈機一動,我其實可以把某個 Eclipse 的外掛,移除 UI,利用它的程式碼。

HAHA 是一個無UI Android 記憶體分析器。基本上就是把另一個人寫的程式碼重新打包。開始的時候,我就是 fork 了一份別的程式碼然後移除了UI部分。兩年前,有人重新fork了我的程式碼,然後添加了 Android 支援。又過了兩年,我才發現這個人的倉儲,然後我又重新打包上傳到了 maven center。

我最近根據 Android Studio 修改了程式碼實現。程式碼還說的過去,還會繼續維護。

LeakCanary 的實現 (19:19)

我們有自己的庫去解析 heap dump 檔案,而且實現的很容易。我們開啟 heap dump,載入進來,然後解析。然後我們根據 key 找到我們的引用。然後我們根據已有的 Key 去檢視擁有的引用。我們拿到例項,然後得到物件圖,再反向推導發現洩漏的引用。

所有的工作實際上都發生在 Android 裝置上。當 LeakCanary 探測到一個 Activity 已經被銷燬掉,而沒有被垃圾回收器回收掉的時候,它就會強制匯出一份 heap dump 檔案存在磁碟上。然後開啟另外一個程序去分析這個檔案得到記憶體洩漏的結果。如果在同一程序做這件事的話,可能會在嘗試分析堆記憶體結構的時候而發生記憶體不足的問題。

最後,你會得到一個通知,點選一下就會展示出詳細的記憶體洩漏鏈。而且還會展示出記憶體洩漏的大小,你也會很明確自己解決掉這個記憶體洩漏後到底能夠解救多少記憶體出來。

相關推薦

LeakCanary 檢測記憶體洩漏

轉載自:https://academy.realm.io/cn/posts/droidcon-ricau-memory-leaks-leakcanary/ 我們的 App 曾經遇到很多的記憶體洩漏導致 OutOfMemoryError 的崩潰,一些甚至是在生產環境。Square 的 Pierr

為什麼使用LeakCanary檢測記憶體洩漏

為什麼要使用LeakCanary? 不再需要的物件依然被引用,導致物件被分配的記憶體無法被回收,這就是記憶體洩漏的原因。 例如:一個Activity例項物件在呼叫了onDestory方法後是不再被需要的,如果儲存了一個引用Activity物件的靜態域,將導致Ac

Android實戰——LeakCanary檢測記憶體洩漏

LeakCanary檢測記憶體洩漏 前言 記憶體洩漏對於初學者們可能是一個陌生的詞語,但是卻頻頻發生於自己的軟體上,只不過自己不知道而已。同理,記憶體溢位也是一個道理。而記憶體洩漏和記憶體溢位常常是面試的考題,所以早點掌握是必不可少的 記憶體洩

檢測記憶體洩漏的常見工具-LeakCanary

見到這個標題有經驗的開發者可能要吐槽我是標題黨了,特別是從Eclipse時代走過來的開發者,以為我一要開始貼那張像**一樣的MAT記憶體模型圖或者AndroidStudio中Monitors下的實時記憶體佔用圖,又要開始分析那一條條剪不斷理還亂的記憶體引用鏈,然後費盡九牛

檢測記憶體洩漏——LeakCanary的使用

<!-- LeakCanary --> <service android:name="com.squareup.leakcanary.internal.HeapAnalyzerService" android:enabled="false" android:process=":leakcan

Vc 檢測記憶體洩漏

https://docs.microsoft.com/zh-cn/visualstudio/debugger/finding-memory-leaks-using-the-crt-library?view=vs-2017   啟用記憶體洩漏檢測 檢測記憶體洩漏是 C/c + + 偵錯程式和 C

Visual Studio中檢測記憶體洩漏的方法

Visual Studio中檢測記憶體洩漏的方法 #include <iostream> //可以定位到發生記憶體洩露 所在的檔案和具體那一行,用於檢測 malloc 分配的記憶體 #define _CRTDBG_MAP_ALLOC #include <s

LeakCanary Android 記憶體洩漏分析利器 原始碼編譯配置mk檔案

LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := \ $(call all-java-files-under, src) LOCAL_SRC_

Instruments的Leaks檢測記憶體洩漏

一,使用Leaks開啟APP,操作APP; 二,配置Leaks; 在Leaks欄目下選擇Cycles & Roots和Call Tree進行分析; 1,Cycles & Roots; 2,C

VC使用CRT除錯功能檢測記憶體洩漏

轉載自https://www.cnblogs.com/kex1n/archive/2011/04/27/2030753.html 方法一: 檢測記憶體洩漏的基本工具是偵錯程式和CRT除錯堆函式。為了使用除錯堆函式,在你的程式中你必須含有下面的說明: #define _CRTDBG_MAP_A

iOS總結-檢測記憶體洩漏庫-MLeaksFinder原始碼解析

MLeaksFinder是WeRead團隊開源的一款檢測iOS記憶體洩漏的AOP框架。 優點:無侵入性  可以構建洩漏堆疊  有白名單機制  擴充套件性 其他一些特殊處理 主要涉及到的是基類NSObject+MemoryLeak.h類別裡面,- (BOOL)wi

linux valgrind 檢測記憶體洩漏

1.模擬記憶體洩漏的情況 2.編譯程式 g++ -g -o file file.c,一定要加-g 3.安裝valgrind   sudo apt-get install valgrind 4.執行程式valgrind --leak-check

系統技術非業餘研究 » gcc mudflap 檢測記憶體越界的問題

我們用C語言在做大型伺服器程式的時候,不可避免的要面對記憶體錯誤的問題。典型的問題是記憶體洩漏,越界,隨機亂寫等問題。 在linux下valgrind是個很好的工具,大部分問題都可以查的到的。但是對於更微妙的越界問題,valgrind有時候也是無能為力的。比如下面的問題。 [[email

linux工具之檢測記憶體洩漏-valgrind

0.前言 記憶體洩漏是c++程式常見的問題了,特別是服務類程式,當系統模組過多或者邏輯複雜後,很難通過程式碼看出記憶體洩漏; valgrind是一個開源的,檢測c++程式記憶體洩漏有效工具,編譯時加上-g選項可以定位到程式碼行,同時還檢查‘野指標’,檢查malloc與fre

Linux mtrace命令檢測記憶體洩漏

*這篇來學習一下Linux中的一個命令——mtrace,它是怎麼用的呢?一起來看看。。。→_→* 記憶體洩漏程式碼 #include <stdio.h> int main() {

android使用Leaks檢測記憶體洩漏

Leaks 記憶體洩漏檢測工具使用 debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2' releaseCompile 'com.squareup.leakcanary:

使用 Android Studio 檢測記憶體洩漏與解決記憶體洩漏問題

  本文在騰訊技術推文上 修改 釋出。     http://wetest.qq.com/lab/view/63.html?from=ads_test2_qqtips&sessionUserType=BFT.PARAMS.195040.TASKID&ADUIN=913337456&a

LeakCanary+Jenkins 記憶體洩漏監控實踐

背景 公司Android產品的OOM崩潰率持續增長,為了檢測出記憶體洩漏問題,決定使用LeakCanary。為了持續發現記憶體洩漏問題,嘗試將LeakCanary與Jenkins相結合。本文著重於LeakCanary與Jenkins的結合,不會對LeakCanary和J

如何用dmalloc檢測記憶體洩漏(原創)

三下五除二就給說完了1.從www.dmalloc.com下載一個rpm包(你也可以下載原始碼包,本人比較懶 :) )2.安裝3.export DMALLOC_OPTIONS=log=logname,debug=0x3注:logname是你要生成記錄的檔名4.在你需要檢測的原始

(轉)使用windbg檢測記憶體洩漏

轉載地址:緣起:作為C++程式設計師,檢測記憶體洩漏是非常痛苦的事情。尤其是看著程式的記憶體在一直增長,你卻無能為力。此時,windbg可以用來檢測記憶體洩漏。配置windbg: 配置symbol檔案路徑: “SRV*d:\symbols*http://msdl.micros