LeakCanary 的内存泄露问题排查

简介: LeakCanary 的内存泄露问题排查

一、引起内存泄露的原因

1.1 内存泄露的原因

内存泄露指的是程序在申请内存之后,没有办法释放掉已经申请到内存,它始终占用着内存,即被分配 的对象可达但无用。在 Android 中内存泄漏的原因大多是由于生命周期较⻓的对象持有生命周期较短的 对象的引用。

1.2 哪几种对象可以作为GC root

我们知道在Java虚拟机中判断一个对象是否可以被回收,有一种做法叫可达性分析算法,如果GC Root到 某个对象还有可达的引用链,那么这个对象就不能被回收。即垃圾回收器不会回收GC Roots以及那些被 它们间接引用的对象。对象可达性分析需要依赖GC Root。JVM中可以用来作为可以作为GC Root的对象 如下:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

我们要特别注意这些GC root 对象的使用,以免造成内存泄露。

1.3 可视化工具LeakCanary 的集成

dependencies {     // debugImplementation because LeakCanary should only run in debug builds. 
 
      debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'
 
 
 
}

二、LeakCanary的核心原理SDK的初始化是在自定义的ContentProvider AppWatcherInstaller 的onCreate方法中进行的。源码分

析的入口在AppWatcherInstaller的onCreate()方法。

ContentProvider 的 onCreate() 的调用是介于 Application 的 attachBaseContext(Context) 和 onCreate() 之间所调用的,而Application 的 attachBaseContext(Context) 方法被调用这就意味 着 Application 的 Context 被初始化了,而 ContentProvider 拿到的 Context 也正就是Application,所以可以在 ContentProvider 的 onCreate() 方法中完成相应的初始化操作。

 

 android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
 
    android:authorities="${applicationId}.leakcanary-installer"
 
    android:enabled="@bool/leak_canary_watcher_auto_install" 
 
   android:exported="false" />

2.1 内存泄露的监控

2.1.1 Leakcanary 默认自动监控的对象有

  • destroyed Activity instances
  • destroyed Fragment instances
  • destroyed fragment View instances
  • cleared ViewModel instances
  • RootViews removed from the windowmanager
  • destroyed services instances

Destroyed Activity 的监控

class ActivityWatcher( private val application: Application, private val reachabilityWatcher: ReachabilityWatcher ) : InstallableWatcher {
 
    private val lifecycleCallbacks = object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
 
          // Activity执行onDestroy时, 触发对Activity的监听. 
 
           override fun onActivityDestroyed(activity: Activity) {
 
                           reachabilityWatcher.expectWeaklyReachable(                           activity, "${activity::class.java.name} received Activity#onDestroy()callback") }
 
}
 
override fun install() { 
 
         application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
 
}
 
override fun uninstall() { 
 
       application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
 
} }

Fragment 对象的监听是通过activity拿到fragmentManager,注册fragment的生命周期监听,在 onFragmentViewDestroyed触发fragment view 的监听,在onFragmentDestroyed中触发触发 fragment的监听。

override fun invoke(activity: Activity) {    val fragmentManager = activity.fragmentManager
 
    fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
 
}

2.1.2 LeakCanary 内存泄露的监控流程WeakReference+ReferenceQueue机制, 执行GC(GcTrigger.Default#runGc, 实际执行

Runtime.getRuntime().gc())后, 如果有对象不在ReferenceQueue中, 就认为发生了泄漏。

2.2 内存镜像的采集

发生内存泄漏时调用的onObjectRetained() 最终会调用到HeapDumpTrigger的checkRetainedObjects 方法,核心代码如下:

....

var retainedReferenceCount = objectWatcher.retainedObjectCount

// 1.泄露对象的数量是否大于0if (retainedReferenceCount > 0) {

// 若大于0,执行 GC 操作,再次确认泄露与否gcTrigger.runGc()retainedReferenceCount = objectWatcher.retainedObjectCount

}

// 2.如果泄露的对象数量少于设置的内存镜像采集的阈值(默认是5个),则不进行采集

if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

// 3.如果两次内存镜像采集的时间间隔少于WAIT_BETWEEN_HEAP_DUMPS_MILLIS(60秒),则不 进行采集

val now = SystemClock.uptimeMillis()val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillisif (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" 

// 4.内存镜像采集dumpHeap(retainedReferenceCount = retainedReferenceCount, retry = true,

reason = "$retainedReferenceCount retained objects, app is $visibility" )

...

dumpHeap() 方法最终调用到Android 系统提供的 Debug.dumpHprofData(heapDumpFile.absolutePath) 方法,将内存中的数据按照hprof的二进制协议 保存在磁盘中。进而获取到内存泄露时的内存映射文件。

!!! 需要注意的是dumpHprofData方法会冻结当前的APP,因为执行hprof.Dump()前,系统会通过 ScopedSuspendAll执行了暂停所有java线程的操作,保证dump过程中内存数据的不变性,dump结束 后通过resumeAll恢复虚拟机的执行。

2.3 hprof文件的解析

获取到内存映射文件heapDumpFile后,会开启HeapAnalyzerService服务(继承于IntentService),在 子线程中通过 shark 插件 完成heapDumpFile文件的解析,找到内存泄露的引用链。核心代码如下:

...

private fun FindLeakInput.analyzeGraph( metadataExtractor: MetadataExtractor, leakingObjectFinder: LeakingObjectFinder, heapDumpFile: File, analysisStartNanoTime: Long

): HeapAnalysisSuccess {

listener.onAnalysisProgress(EXTRACTING_METADATA)val metadata = metadataExtractor.extractMetadata(graph)

val retainedClearedWeakRefCount = KeyedWeakReferenceFinder.findKeyedWeakReferences(graph).filter { it.isRetained && !it.hasReferent }.count()

// This should rarely happens, as we generally remove all cleared weak refs right before a heap dump.

val metadataWithCount = if (retainedClearedWeakRefCount > 0){ metadata + ("Count of retained yet cleared" to"$retainedClearedWeakRefCount KeyedWeakReference instances") }

else { metadata}

      listener.onAnalysisProgress(FINDING_RETAINED_OBJECTS)       // 1.根据对象关系图,找到泄露的对象id.(在graph中存储有所有的gcRoots等)       val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)

      // 2.找到是应用的泄露还是三方SDK的泄露

val (applicationLeaks, libraryLeaks, unreachableObjects) = findLeaks(leakingObjectIds)

return HeapAnalysisSuccess(

heapDumpFile = heapDumpFile,createdAtTimeMillis = System.currentTimeMillis(), analysisDurationMillis = since(analysisStartNanoTime), metadata = metadataWithCount,applicationLeaks = applicationLeaks,libraryLeaks = libraryLeaks,unreachableObjects = unreachableObjects

) }

...

...

//

private fun FindLeakInput.findLeaks(leakingObjectIds: SetLong>): LeaksAndUnreachableObjects {

val pathFinder = PathFinder(graph, listener, referenceMatchers) val pathFindingResults =

pathFinder.findPathsFromGcRoots(leakingObjectIds, computeRetainedHeapSize)

val unreachableObjects = findUnreachableObjects(pathFindingResults, leakingObjectIds)

// 3.查找最短路径val shortestPaths =

deduplicateShortestPaths(pathFindingResults.pathsToLeakingObjects)

val inspectedObjectsByPath = inspectObjects(shortestPaths)

// 4.计算泄露对象占用的内存val retainedSizes =

if (pathFindingResults.dominatorTree != null) {

computeRetainedSizes(inspectedObjectsByPath,pathFindingResults.dominatorTree)

}else {null}

// 5.构建调用栈信息val (applicationLeaks, libraryLeaks) = buildLeakTraces(shortestPaths, inspectedObjectsByPath, retainedSizes )

return LeaksAndUnreachableObjects(applicationLeaks, libraryLeaks, unreachableObjects)

}

...

三、 内存泄露的常⻅场景和解决方案

1.集合类

集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局 性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的 删除机制,很可能导致集合所占用的内存只增不减。常⻅解决方法:在合适的时机进行集合对象的移 除。eg:MainActivity的ondestroy中。

2.单例模式

不正确使用单例模式是引起内存泄露的一个常⻅问题,单例对象在被初始化后将在 JVM 的整个生命周期 中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被 JVM 正常 回收,导致内存泄露。常⻅解决方法是:解决方法是Activity的Context,换成了applicationContext。

以及下载类单例持有Activity,⻓周期持有短周期,导致内存泄露。

3.Rxbus内存泄露 在有生命周期的Android组件中,比如Activity、Fragment中,忘记在onDestroy方法中取消订阅。导致的RxJava的内存泄漏。

4. Handler等非静态内部类内存泄露 非静态内部类持有外部类的引用,导致外部类不能及时被回收,造成内存泄漏。常⻅解决办法:静态内部类+弱引用。

5.定时器TimerTask内存泄露

非静态内部类TimerTask会一直持有外部类Activity 的引用,任务没有执行完毕它所引用的老的 Activity 也不会被销毁,因此就出现了内存泄露的问题。

6.依赖Activity的类资源,在onDestory方法里面没有释放的 进行集合的清理、大对象的回收、监听回调的注销等

目录
相关文章
|
4月前
|
Java 数据库连接
Java中的内存泄漏排查与预防方法
Java中的内存泄漏排查与预防方法
|
4月前
|
监控 Java
Java中的内存泄漏分析与排查技巧
Java中的内存泄漏分析与排查技巧
|
2月前
|
监控 Java Linux
redisson内存泄漏问题排查
【9月更文挑战第22天】在排查 Redisson 内存泄漏问题时,首先需确认内存泄漏的存在,使用专业工具(如 JProfiler)分析内存使用情况,检查对象实例数量及引用关系。其次,检查 Redisson 使用方式,确保正确释放资源、避免长时间持有引用、检查订阅和监听器。此外,还需检查应用程序其他部分是否存在内存泄漏源或循环引用等问题,并考虑更新 Redisson 到最新版本以修复潜在问题。
|
3月前
|
JavaScript Java 开发工具
Electron V8排查问题之接近堆内存限制的处理如何解决
Electron V8排查问题之接近堆内存限制的处理如何解决
211 1
|
6月前
|
缓存 算法 安全
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(二)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
63 0
|
6月前
|
缓存 Java C#
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍(一)
【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍
153 0
|
4月前
|
监控 安全 Java
JVM内存问题之排查Direct Memory泄漏有哪些常用方法
JVM内存问题之排查Direct Memory泄漏有哪些常用方法
116 2
|
4月前
|
监控 Java Linux
JVM内存问题之如果堆内存一直缓慢上涨,如何解决
JVM内存问题之如果堆内存一直缓慢上涨,如何解决
584 1
|
4月前
|
存储 缓存 Java
Android性能优化:内存管理与LeakCanary技术详解
【7月更文挑战第21天】内存管理是Android性能优化的关键部分,而LeakCanary则是进行内存泄漏检测和修复的强大工具。
|
3月前
|
搜索推荐 Java API
Electron V8排查问题之分析 node-memwatch 提供的堆内存差异信息来定位内存泄漏对象如何解决
Electron V8排查问题之分析 node-memwatch 提供的堆内存差异信息来定位内存泄漏对象如何解决
99 0