什么是内存泄露
一些对象有着有限的生命周期。当这些对象所要做的事情完成了,我们希望他们会被回收掉。但是如果有一系列对这个对象的引用,那么在我们期待这个对象生命周期结束的时候被收回的时候,它是不会被回收的。它还会占用内存,这就造成了内存泄露。持续累加,内存很快被耗尽。
比如,当 Activity.onDestroy被调用之后,activity 以及它涉及到的 view 和相关的 bitmap 都应该被回收。但是,如果有一个后台线程持有这个 activity 的引用,那么 activity 对应的内存就不能被回收。这最终将会导致内存耗尽,然后因为 OOM 而 crash。
LeakCanary
LeakCanary 是Square公司开发的一个用于检测OOM(out of memory的缩写)问题的开源库。你可以在 debug 包种轻松检测内存泄露。
Github地址:https://github.com/square/leakcanary
如何使用
引入LeakCanary库 ,在项目的build.gradle文件添加:
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
gradle 强大的可配置性,可以确保只在编译 debug 版本时才会检查内存泄露,而编译 release 等版本的时候则会自动跳过检查,避免影响性能。
在自定义的Application中初始化 ,LeakCanary.install()会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy()之后泄的activity。
如果只想检测Activity的内存泄露,只需要添加这一行代码 。
private static RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
//LeakCanary 就会自动侦测 activity 的内存泄露
// 会返回一个预定义的 RefWatcher ,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。
refWatcher = LeakCanary.install(this);
}
如果还想检测fragment
public class BaseFragment extends Fragment {
//使用 RefWatcher 监控 Fragment:
@Override
public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = MyApplication.getRefWatcher();
refWatcher.watch(this);
}
}
当你在测试debug版本过程中出现内存泄露时,LeakCanary将会自动展示一个通知栏
screenshot.png通过通知里的信息,我们可以解决很多内存泄露问题 。
备注:LeakCanary只支持4.0以上,原因是其中在watch 每个Activity时调用了Application的registerActivityLifecycleCallback函数,这个函数只在4.0上才支持,但是在4.0以下也是可以用的,可以在Application中将返回的RefWatcher存下来,然后在基类Activity的onDestroy函数中调用。
工作原理
简单概述一下, 源码还没有分析明白 。
- RefWatcher.watch()创建一个 KeyedWeakReference 到要被监控的对象 (也就是弱引用)。
- 然后在后台线程检查引用是否被清除,如果没有,调用GC。
- 如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof文件中。
- 在另外一个进程中的 HeapAnalyzerService有一个 HeapAnalyzer使用HAHA 解析这个文件。得益于唯一的 reference key, HeapAnalyzer找到 KeyedWeakReference,定位内存泄露。
- HeapAnalyzer计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
- 引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。
源码层面简单分析
RefWatch
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;
- watchExecutor: 执行内存泄露检测的executor
- debuggerControl :用于查询是否正在调试中,调试中不会执行内存泄露检测
- gcTrigger: 用于在判断内存泄露之前,再给一次GC的机会
- headDumper: 用于在产生内存泄露室执行dump 内存heap
- retainedKeys: 持有那些呆检测以及产生内存泄露的引用的key
- queue : 用于判断弱引用所持有的对象是否已被GC。
- heapdumpListener: 用于分析前面产生的dump文件,找到内存泄露的原因
接下来,我们来看看watch函数背后是如何利用这些工具,生成内存泄露分析报告的。
/**
* Watches the provided references and checks if it can be GCed. This method is non blocking,
* the check is done on the {@link Executor} this {@link RefWatcher} has been constructed with.
*
* @param referenceName An logical identifier for the watched object.
*/
public void watch(Object watchedReference, String referenceName) {
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
// 如果处于debug模式,直接return
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这个方法里面。要理解其工作原理,就得从keyedWeakReference说起
WeakReference与ReferenceQueue
从watch函数中,可以看到,每次检测对象内存是否泄露时,我们都会生成一个KeyedReferenceQueue,这个类其实就是一个WeakReference,只不过其额外附带了一个key和一个name
/** @see {@link HeapDump#referenceKey}. */
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队列中。
在reference类加载的时候,java虚拟机会创建一个最大优先级的后台线程,这个线程的工作原理就是不断检测pending是否为null,如果不为null,就将其放入ReferenceQueue中,pending不为null的情况就是,引用所指向的对象已被GC,变为不可达。
那么只要我们在构造弱引用的时候指定了ReferenceQueue,每当弱引用所指向的对象被内存回收的时候,我们就可以在queue中找到这个引用。如果我们期望一个对象被回收,那如果在接下来的预期时间之后,我们发现它依然没有出现在ReferenceQueue中,那就可以判定它的内存泄露了。LeakCanary检测内存泄露的核心原理就在这里。
监测时机
什么时候去检测能判定内存泄露呢?这个可以看AndroidWatchExecutor的实现
public final class AndroidWatchExecutor implements Executor {
private final Handler backgroundHandler;
public AndroidWatchExecutor() {
mainHandler = new Handler(Looper.getMainLooper());
HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
handlerThread.start();
backgroundHandler = new Handler(handlerThread.getLooper());
}
....
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);
File heapDumpFile = heapDumper.dumpHeap();
if (heapDumpFile == null) {
// Could not dump the heap, abort.
return;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
heapdumpListener.analyze(
new HeapDump(heapDumpFile, reference.key, reference.name, watchDurationMs, gcDurationMs,
heapDumpDurationMs));
}
}
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));
}
}
返回的ActivityResult对象中包含了对象到GC root的最短路径。LeakCanary在dump出hprof文件后,会启动一个IntentService进行分析:HeapAnalyzerService在分析出结果之后会启动DisplayLeakService用来发起Notification 以及将结果记录下来写在文件里面。以后每次启动LeakAnalyzerActivity就从文件里读取历史结果。
参考文档
LeakCanary 中文使用说明
LeakCanary 内存泄露监测原理研究 : 结合源码分析leakcanary检查内存泄露的过程。
网友评论