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方法里面没有释放的 进行集合的清理、大对象的回收、监听回调的注销等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/777723.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

前端正悄悄蚕食后端开发者的工作,这真的好吗?

**前端正悄悄蚕食后端开发者的工作&#xff0c;这真的好吗&#xff1f;** 前端开发者的职责范围正在逐渐扩大。从最初的单纯页面设计&#xff0c;到现在的与后端数据交互、应用逻辑处理等&#xff0c;前端开发者在项目中的作用日益重要。与此同时&#xff0c;这也引发了一个值…

固态,机械,移动(U盘),sd卡,哪个更适合长期储存数据 保存数据用什么硬盘可靠 硬盘数据丢失怎么找回 硬盘维护注意事项

有关硬盘数据丢失的恢复技巧&#xff0c;这篇文章一定要收藏好。在硬盘使用过程中&#xff0c;很多情况都会导致数据丢失&#xff0c;例如硬盘跌落、病毒感染、系统文件损坏等。这时候&#xff0c;一定要采用正确的方法&#xff0c;抢救硬盘中存储的珍贵数据和文档。 有关长期保…

技术实现路径怎么写?(Word项目技术路径文档参考)

软件项目编写技术实现路径至关重要&#xff0c;因为它为项目团队提供了清晰的开发蓝图。这一路径明确了从项目启动到交付各阶段所需的技术方案、步骤及预期成果&#xff0c;有助于团队统一认识&#xff0c;确保开发工作有序进行。同时&#xff0c;技术实现路径有助于识别潜在的…

ELK优化之Filebeat部署

目录 1.安装配置Nginx 2.安装 Filebeat 3.设置 filebeat 的主配置文件 4.修改Logstash配置 5.启动配置 6.kibana验证 主机名ip地址主要软件es01192.168.9.114ElasticSearches02192.168.9.115ElasticSearches03192.168.9.116ElasticSearch、Kibananginx01192.168.9.113ng…

Docker(二):Docker image Docker Container

本文将介绍 Docker 映像和容器以及 docker 文件之间的差异与联系&#xff0c;本文还将解释如何以及何时使用它们。 什么是 Dockerfile&#xff1f; 它是一个简单的文本文件&#xff0c;包含命令或过程的集合。我们运行的这些命令和准则作用于配置为创建新的 Docker 镜像的基本…

G1.【C语言】EasyX初步了解

1.介绍 EasyX 是针对 C/C 的图形库&#xff0c;可以帮助使用C/C语言的程序员快速上手图形和游戏编程。 2.安装 EasyX Graphics Library for CEasyX Graphics Library 是针对 Visual C 的绘图库&#xff0c;支持 VC6.0 ~ VC2019&#xff0c;简单易用&#xff0c;学习成本极低…

轻预压:滚珠丝杆精度与刚性的平衡点!

预压是指在所需的工作负荷下&#xff0c;使滚珠丝杆预先承受一定的负荷&#xff0c;从而使滚珠丝杆的轴向向心度和侧向偏差达到较小的偏差范围&#xff0c;保证了滚珠丝杆的准确性和稳定性&#xff0c;也确保机器的高精度和长期运作的可靠性。 预压是滚珠丝杆设计中的一个重要参…

vue3项目图片压缩+rem+自动重启等plugin使用与打包配置

一、Svg配置 每次引入一张 SVG 图片都需要写一次相对路径&#xff0c;并且对 SVG 图片进行压缩优化也不够方便。 vite-svg-loader插件加载SVG文件作为Vue组件&#xff0c;使用SVGO进行优化。 插件网站https://www.npmjs.com/package/vite-svg-loader 1. 安装 pnpm i vite-svg…

智能与伦理:Kimi与学术道德的和谐共舞

学境思源&#xff0c;一键生成论文初稿&#xff1a; AcademicIdeas - 学境思源AI论文写作 Kimi&#xff0c;由月之暗面科技有限公司开发的智能助手&#xff0c;擅长中英文对话&#xff0c;能处理多种文档和网页内容。在论文写作中&#xff0c;Kimi可提供资料查询、信息整理、语…

JavaWeb--jquery篇

概述 jQuery是一个快速、简洁的JavaScript框架&#xff0c;是一个优秀的JavaScript代码库&#xff08;框架&#xff09;于2006年1月由John Resig发布。它封装JavaScript常用的功能代码&#xff0c;提供一种简便的JavaScript设计模式&#xff0c;优化HTML文档操作、事件处理、动…

Faster-RCNN·代码解读系列01:Selective Search 和 R-CNN、Fast-CNN 简介

Selective Search 和 R-CNN、Fast-CNN 简介 1 目标检测算法简介1.0滑窗法的思路1.1 Selective Search 和 R-CNN 简介1.2.1 Selective Search简介1.1.1 Selective Search的思路1.1.2 Selective Search图解 1.2 Selective Search 和 Fast-CNN简介1.2.1 SPP和ROI Pooling简介1.2.2…

高级计算机体系结构--期末教材复习

Chap2 性能评测和并行编程性能评测并行编程为什么需要三次 barrier改进方法 Chap3 互连网络交换和路由二维网格中 XY 路由 死锁、活锁及饿死死锁避免的方法&#xff1a;虚通道、转弯模型二维网格中最小 西向优先、北向最后和负向优先算法转弯模型&#xff1a;超立方体的部分自适…

原生JavaScript实现录屏功能

1. 前言 使用JavaScript实现浏览器中打开系统录屏功能 示例图: 2. 源码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><…

深度学习——卷积神经网络(convolutional neural network)CNN详解(一)——概述. 步骤清晰0基础可看

在CNN的学习过程中我会提供相应的手算例子帮助理解训练过程。 其他关于神经网络的学习链接如下&#xff1a; 一、了解卷积神经网络 卷积神经网络的作用 总的来说&#xff0c;卷积神经网络的第一个主要作用是对图像进行特征提取&#xff0c;所谓特征提取&#xff0c;就是明白…

7.6第三天作业

一、在数据库中创建一个表student&#xff0c;用于存储学生信息 CREATE TABLE student( id INT PRIMARY KEY, name VARCHAR(20) NOT NULL, grade FLOAT ); &#xff08;1.&#xff09;先创建一个数据库 &#xff08;2.&#xff09;创建student表 查看是否创建成功 1、向studen…

QT c++函数模板与类模板的使用

QT c类模板的使用 #pragma once#include <QtWidgets/QMainWindow> #include "ui_QtWidgetsApplication5.h"class QtWidgetsApplication5 : public QMainWindow {Q_OBJECTpublic:QtWidgetsApplication5(QWidget *parent nullptr);~QtWidgetsApplication5();te…

代码随想录算法训练营第13天|二叉树的递归遍历、二叉树的迭代遍历、二叉树的统一迭代法、102.二叉树的层序遍历

打卡Day13 1.理论基础2.二叉树的递归遍历3.二叉树的迭代遍历3.二叉树的统一迭代法4.102.二叉树的层序遍历扩展107. 二叉树的层序遍历 II199.二叉树的右视图637.二叉树的层平均值429.N叉树的层序遍历515.在每个树行中找最大值116.填充每个节点的下一个右侧节点指针117. 填充每个…

嵌入式C语言面试相关知识——关键字(不定期更新)

嵌入式C语言面试相关知识——关键字 一、博客声明二、C语言关键字1、sizeof关键字2、static关键字3、const关键字4、volatile关键字5、extern关键字 一、博客声明 又是一年一度的秋招&#xff0c;怎么能只刷笔试题目呢&#xff0c;面试题目也得看&#xff0c;想当好厂的牛马其实…

数据可视化之智慧城市的脉动与洞察

在数字化转型的浪潮中,城市作为社会经济发展的核心单元,正经历着前所未有的变革。城市数据可视化大屏看板作为这一变革中的重要工具,不仅极大地提升了城市管理效率,还为公众提供了直观、全面的城市运行状态视图,成为智慧城市建设不可或缺的一部分。本文将深入探讨以“城市…

一文理解 Treelite,Treelite 为决策树集成模型的部署和推理提供了高效、灵活的解决方案

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 一、什么是 Treelite&#xff1f; Treelite 是一个专门用于将决策树集成模型高效部署到生产环境中的机器学习模型编译器&#xff0c;特别适合处理大批量数据的推理任务&#xff0c;能够显著提升推理性能…