leakcanary

  1. 线程栈中的局部变量表引用的所有变量,即运行线程中引用到的所有变量,包括线程中方法参数和局部变量

  2. 存活的线程对象

  3. native 的 jni引用

  4. class 对象 (classLoader 不会卸载class)

  5. 引用类型的静态变量

// 1跟2其实说的是一个东西

Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected.

在检测到适当的可达性改变后,垃圾收集器将注册的引用对象(WeakReference)追加到引用队列(ReferenceQueue)。

​ 核心思路是:
​ leakCanary做法是ondestory后手动出发GC,GC过后对象WeakReference一直不被加入 ReferenceQueue,它可能存在内存泄漏。

​ 利用 双参初始化的弱引用 WeakReference(T referent, ReferenceQueue<? super T> q) 在object对象变成弱可达的时候(可视为已被回收/(其他非强引用也一样)),会将该WeakReference对象入队q中 的特性(也就是queque中最后会存在已经被回收了的weakreference对象),通过在Activity和Fragment的onDestroy()中,将该Activity或Fragment实例的弱引用初始化(双参object,queue)后放入map中(key随机固定uuid),GC后 把map中的 queue包含的对象 移除,map中剩余的即为可能泄露的对象

KOOM

由于leakCanary频繁的Gc时带来的STW(stop the world)会导致明显的卡顿,不适合作为线上监控机制

KOOM采用内存阈值监控来触发镜像采集,将对象是否泄漏的判断延迟到了解析时,阈值监控只要在常驻的子线程中每隔五秒获取关注的几个内存指标。达到阈值则直接suspendAndFork主进程,在新进程(由于fork自主进程,copy_on_write机制因此拥有主进程一样的进程信息)进行dump,之后resumeAndWait主进程。新进程会裁剪dump文件保存。

  • 裁剪时KOOM会根据堆类型进行裁剪:
    • 针对system space(Zygote Space、Image Space):会裁剪PRIMITIVE_ARRAY_DUMP、HEAP_DUMP_INFO、INSTANCE_DUMP和OBJECT_ARRAY_DUMP这4个子TAG,(也就是只保留了CLASS DUMP)会删除这四个子TAG的全部内容(包函子TAG全都会删除)。
    • 针对app space:会处理PRIMITIVE_ARRAY_DUMP这一块数据,但会保留metadata,方便回填。

用于监控应用的 Java 内存泄漏问题,它的核心原理

  • 周期性查询Java堆内存、线程数、文件描述符数等资源占用情况,当连续多次超过设定阈值或突发性连续快速突破高阈值时,触发镜像采集

  • 镜像采集采用虚拟机supend->fork虚拟机进程->虚拟机resume->dump内存镜像的策略,将传统Dump冻结进程20s的时间缩减至20ms以内

  • 基于shark执行镜像解析,并针对shark做了一系列调整用于提升性能,在手机设备测即可执行离线内存泄露判定与引用链查找,生成分析报告

泄露是如何判定的?

  • 对于Activity和Fragment当对象已经销毁却仍有从GC Root到此对象的引用路径时,认为此对象已经泄露

    • Activity的销毁判定规则为,mFinished或mDestroyed值为true

    • Fragment的销毁判定规则为,mFragmentManager为空且mCalled为true,mCalled为true表示此fragment已经经历了一些生命周期

//识别到监控的activity在gc后迟迟不加入ReferenceQueue,会触发dump hprof(Android 设备上,要 dump 当前内存快照,一般会调用 Debug.dumpHprofData() 方法)

Hprof裁剪

主要是用leakcanary的shark组件,LeakCanary 用 shark 组件来实现裁剪 hprof 文件功能,在 shark-cli 工具中,我们可以通过添加 strip-hprof 选项来裁剪 hprof 文件,它的实现思路是:通过将所有基本类型数组替换为空数组(大小不变)。

各家框架基本都会对这份hprof再进行裁剪:

Matrix:Matrix 的裁剪思路主要是将除了部分字符串和 Bitmap 以外实例对象中的 buffer 数组。之所以保留 Bitmap 是因为 Matirx 有个检测重复 Bitmap 的功能,会对 Bitmap 的 buffer 数组做一次 MD5 操作来判断是否重复。

KOOM 通过 xhook 实现 PLT hook,通过 hook 两个虚拟机方法 open()writ() 来实现裁剪 hprof

KOOM 裁剪时会根据堆类型进行裁剪:

  • 针对system space(Zygote Space、Image Space):会裁剪PRIMITIVE_ARRAY_DUMP、HEAP_DUMP_INFO、INSTANCE_DUMP和OBJECT_ARRAY_DUMP这4个子TAG,(也就是只保留了CLASS DUMP)会删除这四个子TAG的全部内容(包函子TAG全都会删除)。
  • 针对app space:会处理PRIMITIVE_ARRAY_DUMP这一块数据,但会保留metadata,方便回填。

//注:LeakCanary2 重写了一个解析 hprof 文件的库,叫做 shark,它是用来代替原来的 haha,根据官方的说法,相比于 haha,shark 内存减少了10倍,速度快了6倍。KOOM 除了使用 shark 来解析,还在这个的基础上做了一些优化,减少了内存的占用,具体可以看源码。

Hprof分析

Matrix:

Hprof文件中包含了Dump时刻内存中的所有对象的信息,包括类的描述,实例的数据和引用关系,线程的栈信息等。具体可参考这份文档中的Binary Dump Format一节。按照文档描述的格式将Hprof中的实例信息解析成描述引用关系的图结构后,套用经典的图搜索算法即可找到泄漏的Activity到GC Root的强引用链了。

大多数时候这样的强引用链不止一条,全部找出来会让一次分析操作的耗时大大增加,延长了整个测试流程的周期,而且对解决问题并没有更多帮助。实际上我们只需要找到最短的那条就可以了。如下图: ref-graph

这种情况下只要切断蓝色箭头即可使泄漏的Activity与GC Root脱离联系。如果持有泄漏的Activity的GC Root不止一个,或者从GC Root出发的引用不止一条,在Matrix框架成为流程化工具的背景下我们可以通过多次检测来解决,这样至少保证了每次执行ResourceCanary模块的耗时稳定在一个可预计的范围内,不至于在极端情况下耽误其他流程。

本来我们打算自行实现这个算法,幸运的是在阅读LeakCanary的代码时我们发现了一个叫haha的库已经把Hprof文件按照文档描述的格式解析成了结构化的引用关系图,而且LeakCanary也按照与上面的描述类似的思路实现了引用链的提取逻辑,于是我们就不再重复造轮子,直接使用了LeakCanary的这部分代码了。

从Hprof文件中获取所有冗余的Bitmap对象

这个功能Android Monitor已经有完整实现了,原理简单粗暴——把所有未被回收的Bitmap的数据buffer取出来,然后先对比所有长度为1的buffer,找出相同的,记录所属的Bitmap对象;再对比所有长度为2的、长度为3的buffer……直到把所有buffer都比对完,这样就记录下了所有冗余的Bitmap对象了,接着再套用LeakCanary获取引用链的逻辑把这些Bitmap对象到GC Root的最短强引用链找出来即可。

美团Probe

Probe 还优化分析泄露对象的链路,因为 Probe 相对于其他方案,理论上它是支持所有对象的内存泄露检测的(排除了原始类型等),而 LeakCanary 和 Matrix 只支持 Activity 和 Fragment 等对象,这个优化是基于 RetainSize 越大的对象对内存的影响也越大,是最有可能造成OOM的“元凶” 这一原则。

首先,在 dump 出所有对象后,创建一个 TOP N 的小根堆,根据 RetainSize 排序,初始 N 默认是 5,这里有个小细节,就是 Probe 会处理 ByteArray 类型的实例:

1
2
3
4
复制代码if (isByteArray(var6)) {
var6.parent.addRetainedSize(var1.getHeapIndex(var4), var6.getTotalRetainedSize());
var15.add(var6.parent);
}

将 ByteArray 的 RetainSize 加到它的父节点,同时将它的父节点加到堆中。

在初始化小根堆后,继续遍历做动态调整,将大于文件大小 5% 的对象直接加到堆中,同时增大 TOP N 的值,否则,跟堆顶元素做比较。在遍历对象的同时,如果是相同类的不同实例,则只会保存一份实例,同时将它们的 RetainSize 和 Num 计算进去:

1
2
3
4
5
复制代码if (var3.containsKey(var9)) {                            
InstanceExtra var17 = (InstanceExtra)var3.get(var9);
var17.retainSize += var6.getTotalRetainedSize();
++var17.num;
}

还会将在 计数压缩逻辑 中 RetainSize 补回来:

1
2
3
4
5
6
7
8
9
复制代码if (var10 != null) {                                                                                  
var16.retainSize += var10.getSize();
long var11 = var16.num;
var16.num = (long)var10.getCount() + var11;
ClassCountInfo var18 = (ClassCountInfo)AbandonedInstanceManager.getInstance().countMap.get(var9);
if (var18 != null && var18.classId > 0L) {
var16.id = var18.classId;
}
}

关于 OOM

Out of memory (OOM) 当系统无法满足申请的内存大小时,就会抛出 OOM 错误。导致 OOM 的原因,除了我们上面说讲的内存泄露以外,可能还会是线程创建超过限制,可以通过 /proc/sys/kernel/threads-max 获取:

1
2
cat /proc/sys/kernel/threads-max
26418

还有一种可能是 FD(File descriptor)文件描述符超过限制,可以通过 /proc/pid/limits 获取,其中 Max open files 就是可创建的文件描述符数量:

1
2
Limit                     Soft Limit           Hard Limit           Units
Max open files 4096 4096 files

LeakCanary 具体原理

​ 具体流程如下:

​ Application中intall(新版本不用,是通过ContenProvider创建时拿到application对象后自动注册)。

​ 然后 通过application.registerActivityLifecycleCallbacks 对每个activity 注册生命周期 onDestroy()回调,
​ 并进一步,根据不同的Android API,兼容地对 AndroidX,AndroidO,Support lib等调用 activity.fragmentManager/activity.supportFragmentManager方法,监听fragment的生命周期 的 onDestroyView() 和 onDestroy()

​ 在 activity/fragment 销毁的生命周期时,调用watch方法。执行removeWeaklyReachableObjects()//“移除watchedObjects中含有的queue元素含有的key
然后将该activity/fragment的弱引用对象KeyWeakReference添加入哈希表watchedObjects(RandomUUID 为key,queue 为弱引用的第二参数队列)。
之后在子线程中再来一次移除弱可达对象removeWeaklyReachableObjects,接下来调用gc(休眠100ms),最后留存下来的watchedObjects 即为大概率泄露的对象

如果 处于调试状态/已检测泄露对象超过5个/据上一次dump时间小于60s return

dump时调用Debug.dumpHprofData()方法,获取堆信息存储到本地文件。然后在HeapAnalyzeService中进行分析(”shark库进行分析”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

//进入destory流程的activity 或 fragment 的弱引用对象map
private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
//即将被顺利销毁的activity 或 fragment的弱引用对象队列
private val queue = ReferenceQueue<Any>()

@Synchronized
fun watch(
watchedObject: Any,
description: String
) {
if (!isEnabled()) {
return
}
removeWeaklyReachableObjects()
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference =
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
//...

watchedObjects[key] = reference
checkRetainedExecutor.execute {
moveToRetained(key)
}
}


@Synchronized
private fun moveToRetained(key: String) {
removeWeaklyReachableObjects()
val retainedRef = watchedObjects[key]
if (retainedRef != null) {
retainedRef.retainedUptimeMillis = clock.uptimeMillis()
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}

private fun removeWeaklyReachableObjects() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
var ref: KeyedWeakReference?
//do while循环,整体就是将watchObjects Map中 含有的 queue成员key的 元素移除
//意在保留有可能泄露的对象 的 弱引用对象
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) { //排除掉 queue中这些即将被回收的弱引用对象,就是有可能会泄露的弱引用对象
watchedObjects.remove(ref.key)
}
} while (ref != null)
}

API

https://square.github.io/leakcanary/recipes/#matching-known-library-leaks

  1. LeakCanary config

val dumpHeap: Boolean = true,
val dumpHeapWhenDebugging: Boolean = false,
val retainedVisibleThreshold: Int = 5,
val referenceMatchers: List = AndroidReferenceMatchers.appDefaults,
val onHeapAnalyzedListener: OnHeapAnalyzedListener = DefaultOnHeapAnalyzedListener.create(),
val metadataExtractor: MetadataExtractor = AndroidMetadataExtractor,
val computeRetainedHeapSize: Boolean = true,
val maxStoredHeapDumps: Int = 7,
val requestWriteExternalStoragePermission: Boolean = false,
val leakingObjectFinder: LeakingObjectFinder = KeyedWeakReferenceFinder,
val useExperimentalLeakFinders: Boolean = false

  1. AppWatcher config

    val watchActivities: Boolean = true,
    val watchFragments: Boolean = true,
    val watchFragmentViews: Boolean = true,
    val watchViewModels: Boolean = true,
    val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5),
    val enabled: Boolean = true

  2. 分离进程分析泄漏
    dependencies { // debugImplementation 'com.squareup.leakcanary:leakcanary-android:${version}' debugImplementation 'com.squareup.leakcanary:leakcanary-android-process:${version}' }

补充

  1. ActivityDestroyWatcher初始化:application注册监听Activity生命周期,并在Activity destroy的时候调用objectWatcher.watch()
  2. FragmentDestroyWatcher初始化:
  • 如果android 8.0以上,注册AndroidOFragmentDestroyWatcher监听fragment生命周期
  • 注册AndroidSupportFragmentDestroyWatcher监听fragment生命周期
  • 注册AndroidXFragmentDestroyWatcher监听fragment生命周期

以上三种本质上都是通过传入的Activity.fragmentManager.registerFragmentLifecycleCallbacks进行监听fragment生命周期回调

  1. 通过InternalLeakCanary类的invoke函数:
  • 初始化一些检测内存泄露过程中需要的对象。
  • addOnObjectRetainedListener设置可能存在内存泄漏的回调。
  • 通过AndroidHeapDumper进行内存泄漏之后进行 heap dump 任务。
  • 通过GcTrigger 手动调用 GC 再次确认内存泄露。
  • 启动内存泄漏检查的线程。
  • 通过registerVisibilityListener监听应用程序的可见性。

实践

基于LeakCanary2.5。

问题:ViewPager切换Fragment时,业务上会将所有展示过的Fragment都加入列表手动缓存,而ViewPager的默认缓存机制是不超过三个,超出的Fragment根据LRU调用其onDestroyView生命周期移除其中的所有View。但对于业务来说我们不希望用户切换回来之后由于ViewPager的缓存限制而重新等待view的创建加载,同时也不希望开发者针对这种情况调用ViewPager的setOffscreenPageLimit()的方式实现。

解决方案:

通过在初始化时通过配置AppWatcher.config关闭LeakCanary对Fragment的泄露监听,然后自己实现一套(CV原代码改动)关于Fragment的泄露监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//先配置,将Fragment及FragmentView的泄露从LeakCanary转到以下自己实现逻辑
AppWatcher.config = AppWatcher.config.copy(watchFragments = false, watchFragmentViews = false)

val fragmentDestroyWatchers = mutableListOf<(Activity) -> Unit>()


//LeakCanary针对AndroidO、AndroidX、AndoridSurport分别处理,此处只以使用较多的AndroidX为例
getWatcherIfAvailable(
"androidx.fragment.app.Fragment",
"com.youself.AndroidXFragmentDestroyWatcher",
objectWatcher,
{ configProvider() }
)?.let {
fragmentDestroyWatchers.add(it)
}

app.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
) {
for (watcher in fragmentDestroyWatchers) {
if (!isIgnoreActivity(activity)) {
watcher(activity)
}
}
}
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//#leakcanary.internal.AndroidXFragmentDestroyWatcher.class

package leakcanary.internal


internal class AndroidXFragmentDestroyWatcher(
private val reachabilityWatcher: ReachabilityWatcher
) : (Activity) -> Unit {

private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

override fun onFragmentCreated(
fm: FragmentManager,
fragment: Fragment,
savedInstanceState: Bundle?
) {
ViewModelClearedWatcher.install(fragment, reachabilityWatcher)
}

/*其实也没啥,就是注释掉这个方法就行了,其他版本兼容的Fragment监听类同样处理
override fun onFragmentViewDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
val view = fragment.view
if (view != null) {
reachabilityWatcher.expectWeaklyReachable(
view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
"(references to its views should be cleared to prevent leaks)"
)
}
}
*/

override fun onFragmentDestroyed(
fm: FragmentManager,
fragment: Fragment
) {
reachabilityWatcher.expectWeaklyReachable(
fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
)
}
}

override fun invoke(activity: Activity) {
if (activity is FragmentActivity) {
val supportFragmentManager = activity.supportFragmentManager
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
ViewModelClearedWatcher.install(activity, reachabilityWatcher)
}
}
}

Author

white crow

Posted on

2021-04-06

Updated on

2024-07-23

Licensed under