LeakCanary 的核心检测机制建立在可达性分析之上——通过 WeakReference + ReferenceQueue 判断对象是否被 GC 回收,五秒延迟后仍未回收即判定为泄漏;ObjectWatcher 负责监控,HeapDumpTrigger 负责决策何时 Dump,整个链路设计精巧且克制
引用计数(Reference Counting)是最直观的垃圾判定策略:每个对象维护一个计数器,记录有多少个引用指向它。当计数器归零时,对象即可被回收。但这种策略有一个致命缺陷——识别不了循环引用。
class Node(val name: String) {
var next: Node? = null
}
fun demonstrateCircularReference() {
val a = Node("A")
val b = Node("B")
a.next = b // B 的引用计数 = 1
b.next = a // A 的引用计数 = 1 (b.next 指向 a)
// 此时 a 和 b 的引用计数都不为 0,即使我们已经无法访问它们
// 如果此处不再有外部引用指向 a 或 b,引用计数法无法回收它们
}
a 和 b 已经不再被外部引用,它们互相持有对方的引用,导致引用计数永远不为零。在真实的 Android 应用中,Activity 持有 View 的引用、View 持有 Context 的引用、Handler 持有 Activity 的引用……对象图远比这个例子复杂,循环引用无处不在。
JVM 采用的 GC 核心是可达性分析(Reachability Analysis),而非引用计数。它的基本思路是:从一组称为 GC Roots 的根对象出发,沿着引用链向下搜索,所有能被到达的对象都是"存活"的,无法到达的对象就是"垃圾"。
GC Roots 是一组特殊的根对象,包括:
以下图为例说明循环引用场景下两种算法的差异:
而两个互相引用的对象 A 和 B,如果没有被任何 GC Root 直接或间接引用,它们就是不可达的——无论它们之间的引用计数是多少:
A.refCount=1 (B指向A) + B.refCount=1 (A指向B)
A 和 B 都无法回收
GC Roots 出发, A 和 B 均不可达
A 和 B 都可回收
LeakCanary 在后续分析 .hprof 文件时(由 Shark 库完成),正是基于可达性分析算法来构建引用链(Reference Chain)。它从 GC Roots 出发,寻找一条到达"泄漏对象"的最短路径。
引用链的最终呈现形式就是我们在 LeakCanary 通知中看到的那条路径,例如:
在 Java 中,一个对象只要被强引用指向,就不会被 GC 回收:
var activity = Activity() // 强引用,GC 不会回收它
WeakReference 是一种弱引用:它指向一个对象,但不阻止 GC 回收它。当没有强引用指向对象时,GC 就会回收它,同时把这个 WeakReference 放入一个 ReferenceQueue:
val queue = ReferenceQueue<Any>()
val ref = WeakReference(mActivity, queue)
// 当 mActivity 被 GC 回收后,ref 会被放入 queue
// 此时 queue.poll() 就能拿到这个 ref
queue.poll() 拿不到它),说明对象仍然被强引用指向——这就是内存泄漏。
ReferenceQueue 是 Java 引用框架的核心组件。它的工作原理如下:
如果步骤 4 一直等不到——即 queue.poll() 始终返回 null——就意味着 target 对象仍然被某条强引用链持有,没有被 GC 回收。这就是内存泄漏的信号。
KeyedWeakReference.kt 代码量不多,但它是整个 LeakCanary 的基石。它继承自 WeakReference,并在其基础上增加了泄漏判定所需的元数据:
class KeyedWeakReference(
referent: Any, // 被监控的对象(如 Activity)
val key: String, // 唯一标识(UUID)
val description: String, // 描述信息(为什么监控它)
val watchUptimeMillis: Long, // 开始监控的时间戳
referenceQueue: ReferenceQueue<Any> // 关联的引用队列
) : WeakReference<Any>(
referent, referenceQueue // 传给父类 WeakReference
) {
@Volatile
var retainedUptimeMillis = -1L // 泄漏持续时间,-1 = 未泄漏
override fun clear() {
super.clear()
retainedUptimeMillis = -1L
}
companion object {
@Volatile
@JvmStatic var heapDumpUptimeMillis = 0L // 最近一次 Heap Dump 的时间戳
}
}
retainedUptimeMillis 的含义:初始值为 -1L,标注该弱引用暂未被判定为泄漏。当五秒延迟后对象仍未被回收,该字段被设为当前时间戳,表示"泄漏已持续的时间"。所有判定逻辑都以这个字段是否等于 -1 为判断依据。
重写的 clear() 方法深入调用链最终到达 Reference.clear0():
clear0() 是 Reference 类中的一个 native 方法,实现在 HotSpot VM 的 C++ 源码中。它的核心作用是主动切断对 referent 的弱引用,而不是等待 GC 来处理。这用于以下场景:
// Reference.java (JDK 源码)
public void clear() {
this.referent = null; // Java 层清空引用
}
// 对应的 native 方法在 HotSpot 中:
// 清除 referent 字段,使得下一次 GC 不会将该引用视为活跃引用
clear()。clear() 是手动调用的,用于在业务流程中主动切断弱引用关系。两者是独立的机制——GC 入队是自动的,clear 是手动的。
ObjectWatcher 实现了 ReachabilityWatcher 接口,是整个监控体系的核心调度者。先看它的核心字段:
class ObjectWatcher constructor(
private val clock: Clock, // 时钟,提供时间戳
private val checkRetainedExecutor: Executor, // 延迟执行器(默认 5s 延迟)
private val isEnabled: () -> Boolean = { true } // 是否启用
) : ReachabilityWatcher {
// 监听器集合:当对象被判定为泄漏时通知它们
private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()
// 核心数据结构:维护所有正在被监控的弱引用
// key = UUID 字符串, value = KeyedWeakReference 实例
private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
// 引用队列:GC 回收对象后自动将 WeakReference 放入此队列
private val queue = ReferenceQueue<Any>()
}
watchedObjects 是整个监控的核心:它是一个 HashMap,key 是创建 KeyedWeakReference 时生成的 UUID,value 是 KeyedWeakReference 实例。它的目的就是维护所有正在被观测的弱引用——如果有引用被回收了(出现在 ReferenceQueue 中),就从 Map 中移除;剩下的就是可能泄漏的。
这是 ObjectWatcher 中最基础也最频繁被调用的方法:
private fun removeWeaklyReachableObjects() {
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference? // 从队列取出已回收的引用
if (ref != null) {
watchedObjects.remove(ref.key) // 从监控 Map 中移除
}
} while (ref != null) // 循环直到队列清空
}
逻辑非常直接:不断从 ReferenceQueue 中 poll,poll 出来的就是已被 GC 回收的对象所对应的 WeakReference。既然已经被回收了,自然就不是泄漏,从 watchedObjects 中移除。循环结束后,Map 中剩下的就是尚未被回收的——它们可能是泄漏,也可能只是 GC 还没来得及回收。
几乎所有公开方法(hasRetainedObjects、retainedObjectCount、hasWatchedObjects、retainedObjects)都会先调用 removeWeaklyReachableObjects(),确保返回的数据是最新的。
这是 ObjectWatcher 最核心的方法,也是外部监控一个对象的唯一入口。它是线程安全的(@Synchronized):
@Synchronized override fun expectWeaklyReachable(
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 // 放入监控 Map
checkRetainedExecutor.execute { // 延迟 5 秒后执行
moveToRetained(key) // 判定是否泄漏
}
}
它的语义非常清晰:
5 秒延迟后执行的方法,是泄漏判定的"终审":
@Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableObjects() // 最后再清理一次
val retainedRef = watchedObjects[key]
if (retainedRef != null) { // key 还在 Map 中 = 未被回收
retainedRef.retainedUptimeMillis = clock.uptimeMillis() // 标记为泄漏
onObjectRetainedListeners.forEach { it.onObjectRetained() } // 通知所有监听器
}
}
逻辑非常简洁:
retainedUptimeMillis 设为当前时间(从 -1 变为正数,标记为泄漏)回顾整个 ObjectWatcher 的设计,它的本质可以总结为三样东西的组合:
维护所有正在被监控的弱引用,以 UUID 为 key 实现 O(1) 查找和移除
接收已被 GC 回收的引用,通过 poll() 持续清空队列来同步回收状态
给 GC 足够的时间回收对象,避免误判。5 秒后仍留在 Map 中的就是泄漏候选
在深入 HeapDumpTrigger 之前,先理解 Heap Dump 的概念:
.hprof 格式)。其中包括对象间的引用关系,通过分析快照即可找出泄漏的对象和引用链。
Heap Dump 是一个昂贵的操作:它会暂停应用,遍历整个堆,将所有对象序列化到磁盘。因此 LeakCanary 需要精心的决策逻辑来避免频繁 Dump。
这是 HeapDumpTrigger 最核心的方法。当 ObjectWatcher 判定有对象泄漏后,最终会调用到这里。它的决策链路如下:
逐步展开来看。首先判断当前环境是否支持 Dump:
private fun checkRetainedObjects() {
val iCanHasHeap = HeapDumpControl.iCanHasHeap()
// ...
if (iCanHasHeap is Nope) {
// Dump 被禁用:通知用户或静默跳过
return
}
// 获取泄漏对象数量
var retainedReferenceCount = objectWatcher.retainedObjectCount
// 如果大于 0,主动触发一次 GC 后再检查
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
// 检查数量是否达到阈值
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
// 检查距离上次 Dump 是否够 60 秒
val now = SystemClock.uptimeMillis()
val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
scheduleRetainedObjectCheck(
delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
)
return
}
// 全部条件通过 → 执行 Dump
dumpHeap(retainedReferenceCount = retainedReferenceCount, retry = true, reason = "...")
}
gcTrigger.runGc() 主动触发一次 GC,然后再获取一次数量。这是为了确保在此期间没有对象被 GC 回收,尽可能避免误判。如果 GC 后数量变为 0,后续的 checkRetainedCount 会直接返回 true(无需 Dump)。
HeapDumpControl.iCanHasHeap() 返回一个密封类,用于表达当前环境是否允许 Dump:
sealed class ICanHazHeap {
object Yup : ICanHazHeap() // 允许 Dump
abstract class Nope(val reason: () -> String) : ICanHazHeap() // 不允许
class SilentNope(reason: () -> String) : Nope(reason) // 静默拒绝
class NotifyingNope(reason: () -> String) : Nope(reason) // 拒绝并通知用户
}
当返回 NotifyingNope 时,HeapDumpTrigger 仍然会先检查是否有泄漏对象,如果有就主动 GC 再确认一次,然后将禁用原因通知给用户。这样即使 Dump 功能被禁用,用户仍然能看到"有 N 个对象泄漏了"的信息。
这个方法决定了"要不要现在 Dump",核心逻辑围绕一个阈值展开:
private fun checkRetainedCount(
retainedKeysCount: Int,
retainedVisibleThreshold: Int, // 默认值为 5
): Boolean {
// ...
if (retainedKeysCount == 0) {
// 没有泄漏 → 通知"全部回收",return true(不需要 Dump)
return true
}
if (retainedKeysCount < retainedVisibleThreshold) { // 少于 5 个
if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
// 应用可见或刚进入后台 → 通知"观察中",2 秒后再查
showRetainedCountNotification(...)
scheduleRetainedObjectCheck(
delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS // 2000ms
)
return true // 暂不 Dump,再等等
}
}
return false // 数量 >= 5 或应用已长时间不可见 → 允许 Dump
}
决策逻辑总结:
当条件不满足时(数量不够或距离上次 Dump 太近),会调用这个方法安排延迟检查:
fun scheduleRetainedObjectCheck(delayMillis: Long = 0L) {
val checkCurrentlyScheduledAt = checkScheduledAt
if (checkCurrentlyScheduledAt > 0) {
return // 已有任务在排队,不重复调度
}
checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
backgroundHandler.postDelayed({
checkScheduledAt = 0
checkRetainedObjects() // 重新走一遍完整决策流程
}, delayMillis)
}
这个 Handler 运行在名为 "LeakCanary-Heap-Dump" 的后台线程上:
// InternalLeakCanary.kt 中的初始化
val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
handlerThread.start()
val backgroundHandler = Handler(handlerThread.looper)
heapDumpTrigger = HeapDumpTrigger(
application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, configProvider
)
checkScheduledAt 充当了一个简单的互斥锁。如果已经有检查任务在排队(值 > 0),新的调度请求会被直接忽略。这避免了在短时间内堆积大量重复的检查任务。
当所有检查都通过后,最终执行 Heap Dump:
private fun dumpHeap(
retainedReferenceCount: Int,
retry: Boolean,
reason: String
) {
val heapDumpFile = directoryProvider.newHeapDumpFile() // 创建 .hprof 文件
val heapDumpUptimeMillis = SystemClock.uptimeMillis() // 记录时间戳
KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
// 执行堆转储(将当前堆中的所有对象写入文件)
configProvider().heapDumper.dumpHeap(heapDumpFile)
// 清理本次 Dump 之前的所有 WeakReference
objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
}
主要逻辑分四步:
第 4 步很重要:Dump 完成后,堆里的 KeyedWeakReference 实例已经写入了 hprof 文件,运行时不再需要这些引用,所以将它们从 watchedObjects 中清理掉。Shark 库后续会读取 hprof 文件进行引用链分析。
与自动监控流程不同,用户点击通知或调用 LeakCanary.dumpHeap() 时走的是这条路径:
fun onDumpHeapReceived(forceDump: Boolean) {
backgroundHandler.post {
gcTrigger.runGc() // 先尝试触发 GC
val count = objectWatcher.retainedObjectCount
if (!forceDump && count == 0) {
return // 非强制 + 无泄漏 → 不 Dump
}
dumpHeap(count, retry = false, reason = "user request")
}
}
将 HeapDumpTrigger 的整个决策过程可视化如下:
LeakCanary 用最少的代码、最简单的机制(WeakReference + ReferenceQueue + 5 秒延迟),配合克制的决策策略(5 个阈值 + 60 秒间隔),构建了一套精准、低开销的内存泄漏检测体系——它的精髓不在于"检测得多快",而在于"在合适的时机做合适的事"。