opoojkk

LeakCanary检测到内存泄漏之后发生了什么

xx
目次

检测 #

LeakCanary 是 Android 开发中最常用的内存泄漏检测工具之一。由于在 dump 内存快照时需要 挂起所有线程,这会带来明显的卡顿,因此它通常只在开发和调试阶段启用。

涉及到的类型有弱引用、引用队列和一个map。像是这样:

1
  private val watchedObjects: MutableMap<String, KeyedWeakReference> = ConcurrentHashMap()

弱引用不会影响对象是否被回收,当弱引用被清理之后,弱引用会被添加进关联的弱引用队列。Leak Canary用到的弱引用类型KeyedWeakReferenceWeakReference的一个子类,增加了key。弱引用的key也是watchedObjects的key,于是对象、弱引用、map几个关键信息就联系起来。

利用带有 key 的弱引用和引用队列,可以识别出哪些对象已经被回收。因为 key 是唯一的,所以一旦弱引用进入引用队列,就能从 watchedObjects 中移除对应的键值对。这样,watchedObjects 中剩下的条目就代表仍未被回收的对象,它们就是潜在的泄漏候选。

LeakCanary 借助 Lifecycle 回调,在 onDestroy() 时机清理引用队列中(已经被回收的)弱引用,并将对应条目从 watchedObjects 中移除。理论上,正常销毁的对象在这一步之后就不会再出现在 watchedObjects 中。随后 LeakCanary 会延迟一段时间(默认 5s)再次检查,如果仍然发现有 Activity 的弱引用留存,说明对象未能被 GC,存在内存泄漏,于是触发 dump 内存快照,并开始分析引用链路。

dump内存快照 #

这一步本质上是调用 Android 提供的 API 来生成 hprof 文件,相对比较“机械”,没有太多自定义逻辑。

分析内存快照 #

dump后发送消息,开始分析内存快照。

  1. 解析拿到header,包含时间戳、版本、标识(分辨字符串、对象、stack traces)信息;
  2. 找到和几种GC Root类型的关联的record,实际上是找到每种类型开始的index,存在一个数据类中一并返回;
  3. 找出泄露对象的id,默认实现先找出KeyedWeakReference类型的实例,再找出判定认为已经泄露的对象;
  4. 计算泄漏对象到 GC Roots 的最短路径,从而定位出内存泄漏的根因链路。

GC Root类型:

找最短路径 #

前面步骤大多是按照 hprof 格式和 tag 规则解析数据,而“最短路径”这一环节则更关键,决定了我们如何解释泄漏对象与 GC Roots 的关系,因此单独展开。

 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
  private fun State.findPathsFromGcRoots(): PathFindingResults {
    enqueueGcRoots()

    val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()
    visitingQueue@ while (queuesNotEmpty) {
      val node = poll()
      if (leakingObjectIds.contains(node.objectId)) {
        shortestPathsToLeakingObjects.add(node)
        // Found all refs, stop searching (unless computing retained size)
        if (shortestPathsToLeakingObjects.size == leakingObjectIds.size()) {
          if (computeRetainedHeapSize) {
            listener.onEvent(StartedFindingDominators)
          } else {
            break@visitingQueue
          }
        }
      }

      val heapObject = try {
        graph.findObjectById(node.objectId)
      } catch (objectIdNotFound: IllegalArgumentException) {
        // This should never happen (a heap should only have references to objects that exist)
        // but when it does happen, let's at least display how we got there.
        throw RuntimeException(graph.invalidObjectIdErrorMessage(node), objectIdNotFound)
      }
      objectReferenceReader.read(heapObject).forEach { reference ->
        val newNode = ChildNode(
          objectId = reference.valueObjectId,
          parent = node,
          lazyDetailsResolver = reference.lazyDetailsResolver
        )
        enqueue(
          node = newNode,
          isLowPriority = reference.isLowPriority,
          isLeafObject = reference.isLeafObject
        )
      }
    }
    return PathFindingResults(
      shortestPathsToLeakingObjects,
      if (visitTracker is Dominated) visitTracker.dominatorTree else null
    )
  }

LeakCanary中查找是有两个队列,分高低优先级,先从高优先级中找出路径,高优先级队为空才会从低优先级队列中找。 贴上关键代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private fun State.poll():ReferencePathNode {
  return if (!visitingLast &&!toVisitQueue.isEmpty()) {
    val removedNode = toVisitQueuepoll()
    toVisitSet.remove(removedNodeobjectId)
    removedNode
  } else {
    visitingLast = true
    val removedNode =toVisitLastQueue.poll()
    toVisitLastSet.remov(removedNode.objectId)
    removedNode
  }
}

toVisitQueuepoll和toVisitLastQueue即是高低优先级的队列。

队列中添加元素是怎么执行的呢?

同样贴上代码:

 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
return sortedGcRoots(graph).asSequence().mapNotNull { (heapObject, gcRoot) ->
      when (gcRoot) {
        // Note: in sortedGcRoots we already filter out any java frame that has an associated
        // thread. These are the remaining ones (shouldn't be any, this is just in case).
        is JavaFrame -> {
          GcRootReference(
            gcRoot,
            isLowPriority = true,
            matchedLibraryLeak = null
          )
        }
        is JniGlobal -> {
          val referenceMatcher = when (heapObject) {
            is HeapClass -> jniGlobalReferenceMatchers[heapObject.name]
            is HeapInstance -> jniGlobalReferenceMatchers[heapObject.instanceClassName]
            is HeapObjectArray -> jniGlobalReferenceMatchers[heapObject.arrayClassName]
            is HeapPrimitiveArray -> jniGlobalReferenceMatchers[heapObject.arrayClassName]
          }
          if (referenceMatcher !is IgnoredReferenceMatcher) {
            if (referenceMatcher is LibraryLeakReferenceMatcher) {
              GcRootReference(
                gcRoot,
                isLowPriority = true,
                matchedLibraryLeak = referenceMatcher
              )
            } else {
              GcRootReference(
                gcRoot,
                isLowPriority = false,
                matchedLibraryLeak = null
              )
            }
          } else {
            null
          }
        }
        else -> {
          GcRootReference(
            gcRoot,
            isLowPriority = false,
            matchedLibraryLeak = null
          )
        }
      }
    }

返回值是 GcRootReference,其中的 isLowPriority 字段决定了该引用进入高优先级还是低优先级队列。规则是:

匹配规则参考:shark:shark.AndroidReferenceMatchers.kt

findPathsFromGcRoots 中可以看到 enqueue 的调用,这是因为 LeakCanary 使用 BFS(广度优先搜索) 来遍历引用链。BFS 的特性保证了:第一次找到的泄漏对象路径就是最短路径,因此无需回溯或额外计算。

标签:
Categories: