本文主要分析内存泄漏的检测原理和如何实现生产环境应用,代码分析基于Leakcanary 1.6版本。
如何检测内存泄漏
要想搞懂如何检测内存泄漏,有一个基本知识点需要知道:
引用类型 | 被垃圾回收时间 | 用途 | 用途 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 当内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | 正常垃圾回收时 | 对象缓存 | 垃圾回收后终止 |
虚引用 | 正常垃圾回收时 | 跟踪对象的垃圾回收 | 垃圾回收后终止 |
这里我们需要用到弱引用,因为弱引用符合我们检测的场景,就是当内存充足的时候触发的系统gc
,弱引用对象也能被回收,这样有利于我们观察对象回收情况,并且弱引用在初始化的时候,有如下构造方法:
WeakReference(T referent, ReferenceQueue<? super T> q)
其中第二个参数是个队列,当对象被回收后会被放入该队列中,我们就可以通过在activity或者fragment onDestroy
的时候,主动触发系统gc
,然后查看队列中是否有该对象,如果没有,我们就认为发生了一次内存泄漏。我们看下leakcanry的具体实现:
public static void install(Context context, RefWatcher refWatcher) {
Application application = (Application) context.getApplicationContext();
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() {
@Override public void onActivityDestroyed(Activity activity) {
refWatcher.watch(activity);
}
};
通过application的registerActivityLifecycleCallbacks
监听所有activity的生命周期。接下去再看下在onActivityDestoryed
中做了什么:
public void watch(Object watchedReference, String referenceName) {
.....
final long watchStartNanoTime = System.nanoTime();
// 随机生成一个key
String key = UUID.randomUUID().toString();
// key放入Set<String>
retainedKeys.add(key);
// new 一个包含key信息的弱引用对象
final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
// 异步监测
ensureGoneAsync(watchStartNanoTime, reference);
}
// 启动异步监测
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run() {
return ensureGone(reference, watchStartNanoTime);
}
});
}
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
// 更新retainedKeys,把存在queue中回收掉的key在retainedKeys去除掉
removeWeaklyReachableReferences();
// 如果当前应用的key已经不在retainedKeys中,说明引用在之前就被gc了
if (gone(reference)) {
return DONE;
}
// gc
gcTrigger.runGc();
// 再次更新retainedKeys
removeWeaklyReachableReferences();
// 这个时候引用的key还在retainedKeys,说明引用已经内存泄漏
if (!gone(reference)) {
......dump操作
}
return DONE;
}
以上是acitivty
的监测方法,接下去我们看下fragment
的相关流程。
// 1、install
public static void install(Context context, RefWatcher refWatcher) {
List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();
if (SDK_INT >= O) {
// os包下面的Fragment
fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));
}
try {
// v4包下面的Fragment,这边用反射因为类放在另外一个module下
Class<?> fragmentRefWatcherClass = Class.forName(SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME);
Constructor<?> constructor = fragmentRefWatcherClass.getDeclaredConstructor(RefWatcher.class);
FragmentRefWatcher supportFragmentRefWatcher = (FragmentRefWatcher) constructor.newInstance(refWatcher);
fragmentRefWatchers.add(supportFragmentRefWatcher);
} catch (Exception ignored) {
}
if (fragmentRefWatchers.size() == 0) {
return;
}
Helper helper = new Helper(fragmentRefWatchers);
Application application = (Application) context.getApplicationContext();
// 添加Activity的生命周期回调
application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
}
// 2、在activity的onActivityCreated生命周期调用的时候添加检测fragment逻辑
private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() {
@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
// 遍历os包和v4包下所有的watcher
for (FragmentRefWatcher watcher : fragmentRefWatchers) {
// 在onActivityCreated的时候给这个activity添加检测fragment逻辑
watcher.watchFragments(activity);
}
}
};
// 3、给activity添加检测fragment的逻辑
@Override public void watchFragments(Activity activity) {
FragmentManager fragmentManager = activity.getFragmentManager();
// 添加递归回调
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
}
// 4、 在fragment的生命周期中检测
private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks = new FragmentManager.FragmentLifecycleCallbacks() {
@Override
public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
// 在fragment的生命周期中检测
refWatcher.watch(fragment);
}
};
接下去具体的watch逻辑就和activity的一模一样,这里就不多赘述了。
生产环境应用
如果把leakcanary直接搬到线上的话有有以下几个问题需要解决:
- dump出来的文件有100M左右甚至更大
-
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
dump方法会冻结应用所有进程,持续数秒到数十秒的卡顿
基于以上两个问题,我们可以参考下几家大厂的解决方案:
微信开源的 Martix 中的Matrix-Android-ResourceCanary
Martix
把分析过程需要用到的部分字符串数据和Bitmap的buffer数组外,其余的buffer数据都直接剔除,这样处理之后的Hprof文件通常能比原始文件小1/10以上。
其他主要增加了一些误报的优化。
美团的 Probe:Android线上OOM问题定位组件
通过 native hook 了虚拟机写入 hprof 文件的操作,在这里先过滤了某些数据,最终生成的 hprof 文件就是裁剪后的了。在不考虑可能会有的兼容性问题,这个方案要比其他的方案要高效,因为它解决了 hprof 加载的内存问题。
但是Probe是闭源的。
快手的 KOOM——高性能线上内存监控方案
快手最近开源的KOOM有这几个亮点:
- 实现了Probe中提出 hook 生成 hprof 的 native 方法
- 解决了
dumpHprofData
方法阻塞问题 - 使用了 LeakCanary2 中使用的 hprof 分析库 shark
对于 dumpHprofData
方法阻塞问题,koom的解决方式是fork新进程来执行dump操作,这样对父进程的正常执行没有影响。以下是耗时对比:
fork dump | normal dump | |
---|---|---|
耗时(ms) | 139.5 | 19962.5 |
综上对比,只有koom解决了线上应用的两大痛点。