LeakCanary监控原理解析

一、引言

最近项目中需要对App的性能进行监控并采集性能数据,在内存泄漏的监控上,采用了LeakCanary的Sdk。
LeakCanary是业界公认的比较好的一个内存监控项目,很早以前就听说并使用过,但是一直没有去了解其背后的监控原理。今天就借这个项目的开发之际,好好的分析一下其原理。

二、原理分析

我们还是从LeakCanary的使用上入手,代码如下:

final RefWatcher refWatcher = LeakCanary.install(this);
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
     ...省略未实现的方法
    @Override
    public void onActivityDestroyed(Activity activity) {
        refWatcher.watch(activity);
    }
});

上面的代码中使用了LeakCanary.install得到了一个RefWatcher对象,我们进入到LeakCanary的install方法,代码如下:

public static @NonNull RefWatcher install(@NonNull Application application) {
    return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
    .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
    .buildAndInstalle();
}

这里使用了建造者模式,其中的listenerServiceClass方法设置了内存泄漏处理的ServiceexcludedRefs方法设置了不对哪些对象进行监听(这里可以设置Android系统的对象和第三方Sdk的对象),最后执行了buildAndInstalle方法生成了一个RefWatcher对象

在生成了RefWatcher对象后,需要就去监听Activity是否存在内存泄漏。这里我们使用了registerActivityLifecycleCallbacks方法,并在ActivityLifecycleCallbacks接口的onActivityDestroyed方法中监听Activity对象,代码如下:

registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
     ...省略未实现的方法
    @Override
    public void onActivityDestroyed(Activity activity) {
        refWatcher.watch(activity);
    }
});

可以看到,这里又调用了RefWatcher对象的watch方法,代码如下:

public void watch(Object watchedReference) {
    this.watch(watchedReference, "");
}

watch方法又调用了watch的重载方法,代码如下:

public void watch(Object watchedReference, String referenceName) {
    if (this != DISABLED) {
        Preconditions.checkNotNull(watchedReference, "watchedReference");
        Preconditions.checkNotNull(referenceName, "referenceName");
        long watchStartNanoTime = System.nanoTime();
        String key = UUID.randomUUID().toString();
        this.retainedKeys.add(key);
        KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
        this.ensureGoneAsync(watchStartNanoTime, reference);
    }
}

上面的代码判断了watchedReference对象和referenceName对象不为空,然后使用UUID.randomUUID().toString()创建了一个全局唯一key值。

对于UUID.randomUUID有疑问的可以看一下UUID.randomUUID()简单介绍 这篇文章

然后new了一个KeyedWeakReference对象,并将watchedReference对象全局唯一的key值引用对象的名字(这里为空)ReferenceQueue对象传入到构造函数中。
而LeakCanary监控内存泄漏的关键就在于KeyedWeakReference这个对象。所以,我们进入它的构造函数中看一下,代码如下:

KeyedWeakReference(Object referent, String key, String name, ReferenceQueue<Object> referenceQueue) {
    super(Preconditions.checkNotNull(referent, "referent"), (ReferenceQueue)Preconditions.checkNotNull(referenceQueue, "referenceQueue"));
    this.key = (String)Preconditions.checkNotNull(key, "key");
    this.name = (String)Preconditions.checkNotNull(name, "name");
}

KeyedWeakReference的构造函数简单的调用了一下其父类WeakReference的构造方法,而其父类WeakReference又调用了其父类的Reference的构造方法。最终是将watchedReference对象和ReferenceQueue对象分别赋值给了Reference的reference和queue

1、reference:
表示引用的对象,也就是上述我们所说的被监控对象。源码对其的描述为/* Treated specially by GC */,表示该对象会被对象特殊处理。而对象被回收后,Reference对象的queue队列会将自己(this对象)进行入队操作。

2、queue:
当该对象中有值时,表示引用的对象reference已经被GC回收。根据这个原理,我们就可以判断那些本应该被回收的对象如果没有被回收,就判断为内存泄漏

好了,让我们从KeyedWeakReference的构造函数回退到上面的watch方法,看看LeakCanary是怎么利用这个KeyedWeakReference的。watch方法最后调用了ensureGoneAsync方法,代码如下:

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    this.watchExecutor.execute(new Retryable() {
        public Result run() {
            return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
        }
    });
}

ensureGoneAsync方法在线程池中执行了ensureGone方法,代码如下:

Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    this.removeWeaklyReachableReferences();
    if (this.debuggerControl.isDebuggerAttached()) {
        return Result.RETRY;
    } else if (this.gone(reference)) {
        return Result.DONE;
    } else {
        this.gcTrigger.runGc();
        this.removeWeaklyReachableReferences();
        if (!this.gone(reference)) {
            long startDumpHeap = System.nanoTime();
            long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
            File heapDumpFile = this.heapDumper.dumpHeap();
            if (heapDumpFile == HeapDumper.RETRY_LATER) {
                return Result.RETRY;
            }

            long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
            HeapDump heapDump = this.heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key).referenceName(reference.name).watchDurationMs(watchDurationMs).gcDurationMs(gcDurationMs).heapDumpDurationMs(heapDumpDurationMs).build();
            this.heapdumpListener.analyze(heapDump);
        }

        return Result.DONE;
    }
}

ensureGone方法执行了下面几个步骤:
1、首先调用了removeWeaklyReachableReferences方法,代码如下:

private void removeWeaklyReachableReferences() {
    KeyedWeakReference ref;
    while((ref = (KeyedWeakReference)this.queue.poll()) != null) {
        this.retainedKeys.remove(ref.key);
    }
}

该方法从queue进行出队列操作,queue.poll() != null 表明该引用对象被GC正常回收了,然后将该对象的标记key从retainedKeys中移除

2、然后判断当前是否处于debug状态,如果是则返回,否则第一次执行gone方法,gone方法的代码如下:

private boolean gone(KeyedWeakReference reference) {
    return !this.retainedKeys.contains(reference.key);
}

逻辑很简单,就是判断一下retainedKeys是否包含引用对象的标记key。如果不包含则表明,该对象被GC正常回收了,因此gone方法返回true,否则存在可疑的内存泄漏则返回false

3、如果第2步返回为false,LeakCanary不会马上触发内存泄漏告警,而是调用了GcTrigger对象的runGc方法主动告知GC,现在应该要回收对象了。

4、调用完GcTrigger对象的runGc方法后,接着又一次调用了removeWeaklyReachableReferences方法,看该对象是否被回收了。

5、然后第二次调用gone方法,如果此时的gone方法还是返回false,则表示该对象存在内存泄漏了。

6、接下来就是将当前时刻的内存堆栈dump下来,并在DisplayLeakService进行分析,然后在DisplayLeakActivity中进行展示。
而对于解析内存堆栈和展示内存泄漏信息的逻辑本文就不做分析了,因为这不属于本文的范畴。有兴趣的同学可以自己读一下源码。

三、总结

LeakCanary内存泄漏的监控原理是利用了Reference对象中的引用对象被回收时,会将其自己存储到ReferenceQueue队列,然后判断ReferenceQueue队列是否有值,来判断对象是否内存泄漏的。

而源码中的WeakHashMap同样的利用了这个原理判断引用的对象是否被回收了,后面有时间可以看一下WeakHashMap的源码进行验证一下。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容