引用计数的有个缺陷就是识别不了循环引用, JVM GC 的核心是可达性分析 :从 GC Roots 出发,判断对象是否仍然可达;不可达的对象才会被回收。理解这一点是读懂 LeakCanary 的前提 ------它后续分析 .hprof 文件时(由 Shark 库完成),正是复用了同样的可达性分析算法来构建引用链,从而定位泄漏根因。如果只有引用计数法,Shark 就无法在存在循环引用的复杂对象图中找到真正的泄漏路径。
LeakCanary 的源码解析(本次解析针对2.14版本),这边强烈额建议去 GitHub 上下载源码或在 Android Studio 导入依赖,一边看源码一边读。
我们知道在 Java 中一个对象只要被强引用指向, 就不会被 GC 回收, 比如:
kotlin
var activity = Activity()
WeakReference是一种弱引用: 它指向一个对象, 但它不阻止GC回收它.当没有强引用指向对象时, GC就会回收它, 同时把这个WeakReference放入一个ReferenceQueue,所以我们可以像这样
kotlin
val queue = ReferenceQueue<Any>()
val ref = WeakReference(mActivity,queue)
当我们调用队列的poll()方法并得到ref对象时, 说明mActivity对象已被GC回收, 而 LeakCanary 正是利用这个机制:延迟一段时间,如果WeakReference仍然没有进入队列,说明对象仍然被强引用指向,从而检测内存泄漏
首先我们看到 KeyedWeakReference.kt , 代码量并不多但是是整个 LeakCanary 的基石,可以看到它继承自 WeakReference ,重写了 clear 方法
kotlin
class KeyedWeakReference(
referent: Any,
val key: String,
val description: String,
val watchUptimeMillis: Long,
referenceQueue: ReferenceQueue<Any>
) : WeakReference<Any>(
referent, referenceQueue
) {
@Volatile
var retainedUptimeMillis = -1L
override fun clear() {
super.clear()
retainedUptimeMillis = -1L
}
companion object {
@Volatile
@JvmStatic var heapDumpUptimeMillis = 0L
}
}
retainedUptimeMillis的意思是"泄漏已持续的时间",初始值为-1标注该弱引用暂时未被判定为泄漏, 接着往下看看到重写的 clear 方法, 如果你深入 super.clear 你最终可以看到最终调用的是Reference类的 clear0() ,它是一个 native 方法,它的实现在 HotSpot VM 的 C++ 源码中, 核心作用就是切断对 referent 的弱引用,而不是等到GC.
再来看到 ObjectWatcher.kt
kotlin
class ObjectWatcher constructor(
private val clock: Clock,
private val checkRetainedExecutor: Executor,
private val isEnabled: () -> Boolean = { true }
) : ReachabilityWatcher {
private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()
private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
private val queue = ReferenceQueue<Any>()
val hasRetainedObjects: Boolean
@Synchronized get() {
removeWeaklyReachableObjects()
return watchedObjects.any { it.value.retainedUptimeMillis != -1L }
}
val retainedObjectCount: Int
@Synchronized get() {
removeWeaklyReachableObjects()
return watchedObjects.count { it.value.retainedUptimeMillis != -1L }
}
val hasWatchedObjects: Boolean
@Synchronized get() {
removeWeaklyReachableObjects()
return watchedObjects.isNotEmpty()
}
val retainedObjects: List<Any>
@Synchronized get() {
removeWeaklyReachableObjects()
val instances = mutableListOf<Any>()
for (weakReference in watchedObjects.values) {
if (weakReference.retainedUptimeMillis != -1L) {
val instance = weakReference.get()
if (instance != null) {
instances.add(instance)
}
}
}
return instances
}
@Synchronized fun addOnObjectRetainedListener(listener: OnObjectRetainedListener) {
onObjectRetainedListeners.add(listener)
}
@Synchronized fun removeOnObjectRetainedListener(listener: OnObjectRetainedListener) {
onObjectRetainedListeners.remove(listener)
}
@Deprecated(
"Add description parameter explaining why an object is watched to help understand leak traces.",
replaceWith = ReplaceWith(
"expectWeaklyReachable(watchedObject, \"Explain why this object should be garbage collected soon\")"
)
)
fun watch(watchedObject: Any) {
expectWeaklyReachable(watchedObject, "")
}
@Deprecated(
"Method renamed expectWeaklyReachable() to clarify usage.",
replaceWith = ReplaceWith(
"expectWeaklyReachable(watchedObject, description)"
)
)
fun watch(
watchedObject: Any,
description: String
) {
expectWeaklyReachable(watchedObject, description)
}
@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)
SharkLog.d {
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
(if (description.isNotEmpty()) " ($description)" else "") +
" with key $key"
}
watchedObjects[key] = reference
checkRetainedExecutor.execute {
moveToRetained(key)
}
}
@Synchronized fun clearObjectsWatchedBefore(heapDumpUptimeMillis: Long) {
val weakRefsToRemove =
watchedObjects.filter { it.value.watchUptimeMillis <= heapDumpUptimeMillis }
weakRefsToRemove.values.forEach { it.clear() }
watchedObjects.keys.removeAll(weakRefsToRemove.keys)
}
@Synchronized fun clearWatchedObjects() {
watchedObjects.values.forEach { it.clear() }
watchedObjects.clear()
}
@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() {
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}
}
watchedObjects是一个 Map ,它的 key 其实就是创建 KeyedWeakReference 所需要的参数 key , 它的目的是维护所有正在被观测的弱引用. 在这里我们就可以看到 LeakCanary 用来判断 KeyedWeakReference 所指向对象是否回收的工具
kotlin
private val queue = ReferenceQueue<Any>()
首先看到第一个方法, hasRetainedObject , 它会调用 removeWeaklyReachableObject() ,而该方法内部会将队列中的元素取出强转为 KeyedWeakReference 类型再来判断该引用指向的对象是否被回收,如果已被回收则将该引用从 watchedObjects 中移除并不断循环直到队列清空,然后返回 watchedObjects 集合中,是否存在任何一个对象,它的 retainedUptimeMillis 值不等于 -1,也就是被判定为retained的检查结果 retainedObjectCount 方法和 hasWatchedObjects 的逻辑差不多, 在这里不过多阐述. 而 retainedObjects 方法的作用则是返回所有被判定为泄漏的对象的强引用列表.
我们接着看到核心方法: expectWeaklyReachable 他是一个线程安全的方法, 它的作用是:
- 对象已经被创建了(比如
Activity调用了onDestroy()) - 我们期望它能被 GC 回收
- 所以创建一个
WeakReference指向它,放入watchedObjects监控 - 如果过了段时间,这个
WeakReference还没被ReferenceQueue回收,说明对象泄漏了
我们接着看到该方法的最后一段代码
kotlin
checkRetainedExecutor.execute {
moveToRetained(key)
}
它在LeakCanary初始化时是这样传入的
kotlin
AppWatcher.objectWatcher = ObjectWatcher(
clock = Clock.uptimeMillis,
checkRetainedExecutor = {
//retainedDelayMillis值默认5s
mainHandler.postDelayed(it, AppWatcher.config.retainedDelayMillis)
},
isEnabled = { AppWatcher.config.enabled }
)
也就是该执行器会在5s之后在主线程执行 moveToRetained(key) 方法 我们再看到 moveToRetained 方法,它依然会先清理 watchedObjects 中所有被回收的对象, 从而根据该 watchedObjects[key] 是否为空来判断被指向的对象是否泄漏,如果泄漏则
- retainedUptimeMillis = now (泄漏)
- 通知所有 listener → 触发 heap dump
所以 ObjectWatcher 的本质其实就是通过 Map 和 ReferenceQueue 和五秒延迟(给GC足够的时间回收)来得到那些剩下的在 Map 中的泄漏对象
接下来要看的类是 HeapDumpTrigger ,这边建议去 github 下载2.14版本的压缩包或者是在 AndroidStudio导入依赖打开HeapDumpTrigger.kt 查看源码, 此处完整源码就不贴出来了, 只贴出关键部分, 因为太长了,Heap Dump (堆转储) 是 Java 虚拟机(JVM/ART)堆内存中所有对象、类、引用关系和实例数据的一份完整快照文件,其中包括对象间的引用关系,通过分析快照即可找出泄漏的对象和引用链.
我们来分析第一个方法 checkRetainedObjects()
kotlin
private fun checkRetainedObjects() {
val iCanHasHeap = HeapDumpControl.iCanHasHeap()
val config = configProvider()
if (iCanHasHeap is Nope) {
if (iCanHasHeap is NotifyingNope) {
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
val nopeReason = iCanHasHeap.reason()
val wouldDump = !checkRetainedCount(
retainedReferenceCount, config.retainedVisibleThreshold, nopeReason
)
if (wouldDump) {
val uppercaseReason = nopeReason[0].toUpperCase() + nopeReason.substring(1)
onRetainInstanceListener.onEvent(DumpingDisabled(uppercaseReason))
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = uppercaseReason
)
}
} else {
SharkLog.d {
application.getString(
R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
)
}
}
return
}
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
val now = SystemClock.uptimeMillis()
val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
onRetainInstanceListener.onEvent(DumpHappenedRecently)
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
)
scheduleRetainedObjectCheck(
delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
)
return
}
dismissRetainedCountNotification()
val visibility = if (applicationVisible) "visible" else "not visible"
dumpHeap(
retainedReferenceCount = retainedReferenceCount,
retry = true,
reason = "$retainedReferenceCount retained objects, app is $visibility"
)
}
首先他会判断当前环境是否支持 Dump ,比如 LeakCanary 是否被启用,当前是否是调试状态等,他返回的是一个ICanHazHeap密封类
kotlin
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)//拒绝但通知用户
}
可以看到在通知用户 LeakCanary 不能 dump heap 之前, 它会先检查一遍是否仍然有泄漏的对象. 之后它会调用 ObjectWatcher 也就是我们刚刚分析过的类的 retainedObjectCount 方法来获取一次泄漏对象的数量, 如果大于0则主动触发GC再进行一次回收再去获取一次.确保在此期间没有被判定为泄漏的对象被回收. 然后它会调用 checkRetainedCount() 方法,如果返回值为 true ,则直接 return , 这个放着待会再看. 如果 checkRetainedCount 方法返回 false , 则获取当前时间戳并计算距离上次 Heap Dump 过去了多久是否满足两次 Heap Dump 间隔不小于60s的条件, 如果不满足则再通知用户泄漏对象数量并调用 scheduleRetainedObjectCheck 方法传入距离下次允许 Heap Dump 的时间还要多久然后返回,否则直接调用 dumpHeap 方法发送通知并开始分析内存泄漏.
我们接下来依次看到 checkRetainedCount() , scheduleRetainedObjectCheck() 和 dumpHeap() 方法 checkRetainedCount() 方法太长,这里只贴出关键逻辑,删去了提示信息相关代码
kotlin
private fun checkRetainedCount(
retainedKeysCount: Int,
retainedVisibleThreshold: Int,
nopeReason: String? = null
): Boolean {
val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
lastDisplayedRetainedObjectCount = retainedKeysCount
if (retainedKeysCount == 0) {
if (countChanged) {
SharkLog.d { "All retained objects have been garbage collected" }
onRetainInstanceListener.onEvent(NoMoreObjects)
showNoMoreRetainedObjectNotification()
}
return true
}
val applicationVisible = applicationVisible
val applicationInvisibleLessThanWatchPeriod = applicationInvisibleLessThanWatchPeriod
if (retainedKeysCount < retainedVisibleThreshold) {
if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
if (countChanged) {
onRetainInstanceListener.onEvent(BelowThreshold(retainedKeysCount))
}
showRetainedCountNotification(
objectCount = retainedKeysCount,
contentText = application.getString(
R.string.leak_canary_notification_retained_visible, retainedVisibleThreshold
)
)
scheduleRetainedObjectCheck(
delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS
)
return true
}
}
return false
}
首先它会判断 lastDisplayedRetainedObjectCount (当前展示给用户看到的泄漏数量)是否与 retainedKeysCount (调用该方法者提供的检测到的泄漏数量)一致, 并将之对齐. 然后它会判断 retainedKeysCount 是否小于 retainedVisibleThreshold (该变量值为5),如果小于并且应用可见或者刚进入后台则进入下一层判断,否则 return false ,下一层判断接着判断泄漏数量如果改变则调用 onRetainInstanceListener.onEvent() 方法.无论是否改变都通知用户并且调用 scheduleRetainedObjectCheck 方法, delayMillis = WAIT\_FOR\_OBJECT\_THRESHOLD\_MILLIS 也就是 2\_000L ,最后 return true .

再来看到 scheduleRetainedObjectCheck
kotlin
fun scheduleRetainedObjectCheck(
delayMillis: Long = 0L
) {
val checkCurrentlyScheduledAt = checkScheduledAt
if (checkCurrentlyScheduledAt > 0) {
return
}
checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
backgroundHandler.postDelayed({
checkScheduledAt = 0
checkRetainedObjects()
}, delayMillis)
}
它首先会检查当前是否已有检查任务在排队, 有的话直接 return , 然后记录调度时间然后在后台 HandlerThread 中延迟执行, 我们可以看到该 HandlerThread 的创建逻辑在 InternalLeakCanary.kt 的invoke() 方法中
kotlin
val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
handlerThread.start()
val backgroundHandler = Handler(handlerThread.looper)
heapDumpTrigger = HeapDumpTrigger(
application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger,
configProvider
)
接着看 heapDump() 方法,为了快速理解核心逻辑, 我删除了大部分代码, 建议可以去对照源码阅读
kotlin
private fun dumpHeap() {
val heapDumpFile = directoryProvider.newHeapDumpFile()
val heapDumpUptimeMillis = SystemClock.uptimeMillis()
KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
configProvider().heapDumper.dumpHeap(heapDumpFile)
objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
}
其主要逻辑便是:创建 .hprof 文件->记录当前 Dump 时间戳->执行堆转储->清理本次 Dump 之前的所有弱引用 , dump 以及把堆里的 KeyedWeakReference 实例写入了 hprof 文件, 这些引用已经归档到文件中, 运行时不再需要.
我们最后看到 onDumpHeapReceived 方法,他是用户点击通知或调用 LeakCanary.dumpHeap() 时的入口
kotlin
fun onDumpHeapReceived(forceDump: Boolean) {
backgroundHandler.post {
gcTrigger.runGc() // 先尝试触发 GC
val count = objectWatcher.retainedObjectCount
if (!forceDump && count == 0) {
// showNoMoreRetainedObjectNotification()
return // 非强制 + 没对象 → 不 dump
}
dumpHeap(count, retry = false, reason = "user request")
}
}
注意区分两种触发路径:
- 自动监控流程 :
checkRetainedCount()在retainedCount < 5且 App 可见时,发出"waiting until 5 retained objects"通知; - 用户手动触发 :用户点击上述通知,或直接调用
LeakCanary.dumpHeap(),才会进入onDumpHeapReceived(forceDump = true)。此时即使只有 2 个泄漏对象,也会强制执行Heap Dump,不受阈值限制。
其核心逻辑如下: 先尝试触发 GC(gcTrigger.runGc()) ,尽可能回收可释放对象; 获取当前泄漏对象数量:val count = objectWatcher.retainedObjectCount ; 若 非强制触发 (forceDump = false) 且无泄漏对象 (count == 0),则直接返回,不执行 Heap Dump ; 否则,调用 dumpHeap(count, retry = false, reason = "user request") 强制生成堆转储文件。
HeapDumpTrigger的决策树

最近忙着找实习, 剩下的部分以后有时间再发。