一文带你搞懂内存泄漏!!!(代码片段)

冬天的毛毛雨 冬天的毛毛雨     2023-01-16     601

关键词:

好文推荐
作者:codelang

检测内存是否泄漏非常简单,只要在任意位置调用 Debug.dumpHprofData(file) 即可,通过拿到 hprof 文件进行分析就可以知道哪里产生了泄漏,但 dump 的过程会 suspend 所有的 java 线程,导致用户界面无响应,所以又不能随意 dump。为了能找到合理的 dump 时机,leakCanary 就采用预判的方式,在 onDestroy 中先检测一下当前 Activity 是否存在泄漏的风险,如果有这种情况,就开始 dump。需要注意的是,在 onDestroy 做检测仅仅只是预判,一种时机,并不能断定真的发生了泄漏,真正的泄漏需要通过分析 hprof 文件才能知晓。 ​

hprof 是由 JVM TI Agent HPROF 生成的一种二进制文件,文件格式可以查看 Binary Dump Format

一、如何预判内存泄漏

  • 主动检测法
  • 阈值检测法

1、主动检测法

  • Activity 的检测预判
  • Service 的检测预判
  • Bitmap 大图的检测预判

1、Activity 的检测预判 LeakCanary 中对 Activity 的预判是在 onDestroy 生命周期中通过弱引用队列来持有当前 Activity 引用,如果在主动触发 gc 之后,泄漏对象集合中仍然能找到该引用实例,则说明发生了内存泄漏,就开始 dump ​

2、Service 的检测预判 LeakCanary 对 Service 的内存泄漏检测时机,是 hook 监听 ActivityThread 的 stopService,然后记录这个 binder 到弱引用集合中,然后代理 AMS 的 serviceDoneExecuting 方法,通过 binder 在弱引用集合中去移除,移除成功的话,说明发生了内存泄漏,就开始 dump ​

3、Bitmap 大图检测预判 Bitmap 不像 Activity、Service 这种,能够通过生命周期主动监测当前是否有内存泄漏的可能,他一般是在 Activity、Service 发生泄漏 dump 的时候,顺便检测一下 Bitmap 。在 Koom 中,Bitmap 大图检测是分析 hprof 中是否有超过 Bitmap 设置的阈值 size (width * height) ​

2、阈值检测法

阈值检测法的代表框架是 Koom,他抛弃了 LeakCanary 的实时检测性,采用定时轮训检测当前内存是否在不断累加,增长达到一定次数(可自己配置)时会进行 dump hprof,这种方式会牺牲一定的时效性,但对于应用到线上的 Koom 的框架,他完全不需要这么高的时效性 ​

二、如何分析内存泄漏

分析工具代表:

  • MAT
  • Android Studio
  • HaHa
    • Matrix
    • LeakCanary 1.x
  • shark
    • Liko
    • Koom
    • LeakCanary 2.x

1、MAT

MAT 工具下载可点击链接 ,Android 生成的 dump 需要做一下转换才能被 MAT 识别,转换指令:

hprof-conv <hprof 文件> <新生成的文件>

eg:

hprof-conv android.hprof mat.hprof

hprof-conv 跟 adb 在同一个文件夹下,配置了 adb 命令的可以直接用这个命令执行。 ​

MAT 查内存泄漏会有点费劲,毕竟是个 java 通用工具,并不会指明告诉你是哪个 Activity 发生了泄漏,但可以分析个大概。 ​

一般泄漏的都是比较大的实例:

点击类名进入查看: ​

ActivityLeakMaker 占用了近 190944 byte 的内存空间,并且引用链里面有 Activity 相关的内容,切回代码来看问题,原来是静态变量持有了 Activity 实例导致:

2、Android Studio

Android Studio 的 Profiler 工具支持 hprof 的解析,并且很智能的提示当前 leak 了哪些对象,打开方式很简单,将 hprof 文件拖拽至 as,然后双击 hprof 文件即可:

我们可以很直观的看到,当前 LeakedActivity 和 ReportFragment 发生了泄漏。 ​

如果我们的需求仅仅只是在开发阶段进行内存泄漏检测的话,并且又不想接入 LeakCanary(因为有时候想调试下自己模块的代码,其他模块经常报内存泄漏,冻结当前线程,很影响调试),那么我们可以在应用里面埋个彩蛋,比如单击 5 次版本号,然后调用 Debug.dumpHprofData ,然后将 hprof 文件导出到 as 进行分析,这就将原本可能会进行数次 dump 的过程,改成了自己需要去检测的时候再去 dump。 ​

3、HaHa

在 LeakCanary 的第一版的时候,是采用的 Haha 库来分析泄漏引用链,但由于后面新出的 Shark,比 HaHa 快 8 倍,并且内存占用还要少 10 倍,但查找泄漏路径的大致步骤与 Shark 无异,故此文就不分析 HaHa 了。 ​

4、Shark

Shark 是 square 团队开发的一款全新的分析 hprof 文件的工具,其官方宣布比 Android Studio 用于 memory profiler 的核心库 perflib 要快 8 倍并且内存占用少 10 倍,更加适合手机端的分析工具。其目的就是提供快速解析hprof文件和分析快照的能力,并找出真正的泄漏对象以及对象到GcRoot 的最短引用路径链,以便帮助开发者更加直观的找出泄漏的真正原因。 – 引用自《LeakCanary2.0解析

看了下 Koom 分析引用链的过程,大致可以分为以下几个步骤:

  • 分析 hprof 文件,获取镜像所有的 instance 实例
  • 遍历所有的实例,判断这个实例与各个 Detectors 是否有存在泄漏,如果有,则记录 objectId 到集合
  • 根据 objectId 集合获取各个泄漏实例引用链,分析出 gcRoot,并遍历 gcRoot 下的引用路径

这个地方重点在于如何找到泄漏的 objectId,因为找到 objectId,即可找到泄漏引用链。在分析 hprof 的时候我们可以拿到 dump 时的内存实例,那么,我们可以根据这个实例来判断是否泄漏,例如:

  • Activity : 判断实例是否是 android.app.Activity 的子类,并且 mFinished 或 mDestroyed 是否为 true (Activity 关闭时该值会为 true),因为 Activity 不泄露的话肯定是会被释放,所以,不可能存在于 dump 的实例中,有就是发生了泄漏
  • Bitmap : 获取实例的类名称是否为 android.graphics.Bitmap,如果是的话,则获取实例的 mWidth 和 mHeight 实例变量,计算两者的乘积是否超过阈值,是的话,也判定为泄漏
  • … (更多判断可以看 analysis 目录的各个 Detector)

Shark 根据 objectId 分析出的引用链路径:

   ┬───
   │ GC Root: Local variable in native code
   │
   ├─ android.os.HandlerThread instance
   │    Leaking: UNKNOWN
   │    ↓ HandlerThread.contextClassLoader
   │                    ~~~~~~~~~~~~~~~~~~
   ├─ dalvik.system.PathClassLoader instance
   │    Leaking: UNKNOWN
   │    ↓ PathClassLoader.runtimeInternalObjects
   │                      ~~~~~~~~~~~~~~~~~~~~~~
   ├─ java.lang.Object[] array
   │    Leaking: UNKNOWN
   │    ↓ Object[].[197]
   │               ~~~~~
   ├─ com.kwai.koom.demo.leaked.ActivityLeakMaker$LeakedActivity class
   │    Leaking: UNKNOWN
   │    ↓ static ActivityLeakMaker$LeakedActivity.uselessObjectList
   │                                              ~~~~~~~~~~~~~~~~~
   ├─ java.util.ArrayList instance
   │    Leaking: UNKNOWN
   │    ↓ ArrayList.elementData
   │                ~~~~~~~~~~~
   ├─ java.lang.Object[] array
   │    Leaking: UNKNOWN
   │    ↓ Object[].[0]
   │               ~~~
   ╰→ com.kwai.koom.demo.leaked.ActivityLeakMaker$LeakedActivity instance
  •     Leaking: YES (This is the leaking object), Signature: 39f4102649e5d3a5be12db591c2e5f68a1c0d2e9

三、如何应用于线上

1、解决 dump 冻结问题

由于 dump hprof 会暂停所有 java 线程问题,致使 LeakCanary 只能应用于线下检测。但 Koom 和 Liko 另辟蹊径,采用 linux 的 copy-on-write 机制,从当前的主线程 fork 出一个子进程,然后在子进程进行 dump 分析,对于用户所在的进程不会有任何感知。 ​

这个地方会有个坑,就是在 fork 子进程的时候 dump hprof。由于 dump 前会先 suspend 所有的 java 线程,等所有线程都挂起来了,才会进行真正的 dump。由于 copy-on-write 机制,子进程也会将父进程中的 threadList 也拷贝过来,但由于 threadList 中的 java 线程活动在父进程,子进程是无法挂起父进程中的线程的,然后就会一直处于等待中。 ​

为了解决这个问题,Koom 和 Liko 采用欺骗的方式,在 fork 子进程之前,先将父进程中的 threadList 全部设置为 suspend 状态,然后 fork 子进程,子进程在 dump 的时候发现 threadList 都为挂起状态了,就立马开始 dump hprof,然后父进程在 fork 操作之后,立马 resume 恢复回 threadList 的状态 ​

2、解决混淆问题

Shark 支持混淆反解析,思路也很简单,解析 mapping.txt 文件,每次读取一行,只解析类和字段:

  • 类特征 :行尾为 : 冒号结尾,然后根据 -> 作为 index 分割,左边的为原类名,右边的为混淆类名
  • 字段特征:行尾不为 : 冒号结尾,并且不包含 (括号(带括号的为方法),即为字段特征,根据 -> 作为 index 分割,左边为原字段名,右边的为混淆字段名

将混淆类名、字段名作为 key,原类名、原字段名作为 value 存入 map 集合,在分析出内存泄漏的引用路径类时,将类名和字段名都通过这个 map 集合去拿到原始类名和字段名即可,即完成混淆后的反解析 ​

leakCanary 内部是写死的 mapping 文件为 leakCanaryObfuscationMapping.txt,如果打开该文件失败,则不做引用链反解析:

也即意味着,如果想 LeakCanary 支持混淆反解析,只需要将自己的 mapping 文件重命名为 leakCanaryObfuscationMapping.txt,然后放入 asset 目录即可

对于 Koom 的混淆反解析,Koom 并没有做,但我们可以自己去加这块代码:

private boolean buildIndex() 
    ...
    try 
      // 新增 ---------- start
      InputStream is =  KGlobalConfig.getApplication().getResources().getAssets().open("mapping.txt");
      ProguardMapping mapping = new ProguardMappingReader(is).readProguardMapping();
     //  新增 ---------- end

      heapGraph = HprofHeapGraph.Companion.indexHprof(hprof, mapping,
              kotlin.collections.SetsKt.setOf(gcRoots));
     catch (Exception e) 
      e.printStackTrace();
    
    return true;
          

将 mapping.txt 文件放到 asset 目录即可,如下是混淆与混淆反解析的引用链的对比:

3、泄漏兜底

在预判内存泄漏发生时,我们可以将 Activity 中引用到的 Bitmap、DrawingCache 等进行主动释放,以此来降低泄漏的影响面。做法是,在 Activity onDestory 时候从 view 的 rootview 开始,递归释放所有子 view 涉及的图片、背景、DrawingCache、监听器等等资源,让 Activity 成为一个不占资源的空壳,泄露了也不会导致图片资源被持有,eg:

...
    Drawable d = iv.getDrawable();
if (d != null) 
    d.setCallback(null);
        
iv.setImageDrawable(null);
...
...

但这一点对于阈值检测法的 Koom 来说,没办法做到,因为他拿不到 onDestroy 时的 Activity 实例,但也不要紧,我们可以将兜底操作做成通用操作,不管他泄漏与不泄露,都做 view 相关引用的卸载。

四、总结:

整体下来,分析个内存泄漏其实并不难,难就难在我们平时并没有养成好的习惯,对于引用的传递考虑的不周全,但我们可以加强自身的编码习惯,尽量减少项目中的泄漏问题

注:本文分析的 Koom 源码为 1.0 版本,目前 Koom 已经出 2.0 版本

一文带你搞懂rpc到底是个啥(代码片段)

RPC(RemoteProcedureCall),是一个大家既熟悉又陌生的词,只要涉及到通信,必然需要某种网络协议。我们很可能用过HTTP,那么RPC又和HTTP有什么区别呢?RPC还有什么特点,常见的选型有哪些?1.RPC... 查看详情

云原生一文带你搞懂docker容器的核心基石cgroups(代码片段)

目录大家好,本文是对Docker容器的核心基石Cgroups的详细讲解,讲解了Cgroups的相关概念、Cgroups的构成与作用、如何查看和使用Cgroups等,对大家后续理解容器有很大的帮助~1、为什么要了解Cgroups2、Cgroups简介3、什么是Cg... 查看详情

一文带你搞懂c++如何操作文件对话框(附源码)(代码片段)

    在C++程序中有时需要通过系统的文件对话框去操作文件或者文件夹,我们有必要熟练掌握操作文件对话框的细节,去更好的为我们软件服务。本文我们就来讲述一下C++在操作文件夹对话框的相关细节ÿ... 查看详情

javascript中的浅拷贝深拷贝是什么?一文带你搞懂,不再犯错(代码片段)

⭐️本文首发自前端修罗场,是一个由资深开发者独立运行的专业技术社区,我专注Web技术、模拟面试、简历修改、Web3、区块链以及职业发展。创作的《前端面试复习笔记》(点击阅读原文订阅),广受好评&... 查看详情

一文就带你搞懂✨为什么重写equals时必须重写hashcode方法?(代码片段)

文章目录🔥整体结构图✨前景说明⭐为什么?有什么作用OR好处?🚀实战演练没有重写hashcode()解决办法:重写hashcode()😘尾言🔥整体结构图✨前景说明为什么重写equals时必须重写hashCode方法?相信... 查看详情

微信小程序一文带你搞懂小程序的页面配置和网络数据请求(代码片段)

文章目录页面配置页面配置文件的作用页面配置和全局配置的关系页面配置网络数据请求网络数据请求的限制配置request合法域名发起get/post请求在页面刚加载时请求数据跳过request合法域名校验关于跨域和ajax请求页面配置每个小... 查看详情

一文带你搞懂“数据”在内外网环境下怎么流通

大学还没毕业,刚出来实习,在搞网络安全的同时我连内外网网络数据包的走向,我都搞不明白怎么去搞安全,所以下面咱们一起学习网络数据包在内外网环境下是怎么进行流通的。1.内网和外网的含义       ... 查看详情

javascript中的浅拷贝深拷贝是什么?一文带你搞懂,不再犯错(代码片段)

⭐️本文首发自前端修罗场,是一个由资深开发者独立运行的专业技术社区,我专注Web技术、模拟面试、简历修改、Web3、区块链以及职业发展。创作的《前端面试复习笔记》(点击阅读原文订阅),广受好评&... 查看详情

爆肝好文!带你搞懂hadoop序列化(代码片段)

Hadoop序列化1.序列化概述2.自定义bean对象3.序列化案例实操1.序列化概述1)什么是序列化序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输。反... 查看详情

一文带你搞懂如何优化慢sql(代码片段)

最近通过SGM监控发现有两个SQL的执行时间占该任务总执行时间的90%,通过对该SQL进行分析和优化的过程中,又重新对SQL语句的执行顺序和SQL语句的执行计划进行了系统性的学习,整理的相关学习和总结如下;作者:京东科技 ... 查看详情

一篇文章带你搞懂js对象的自我销毁(代码片段)

在日常的JS组件开发中,往往会有一些较为复杂的DOM操作及事件监听,尤其是在处理UI层面的widgets时候更为明显。常常会花很多精力在对象的init上,而当组件需要被移除时则仅仅是把所在DOM草草的remove掉就算完事儿。当然,绝大... 查看详情

app自动化测试很重要?一文带你搞懂怎么测app

现在是移动互联网的时代,移动端APP产品在不断推陈出新,相应的APP测试人员也变得炙手可热。今天小濠跟大家全面谈谈APP应该怎么做测试。一、首先,移动测试要注意以下几点:移动APP测试中如何设计TestCaseÿ... 查看详情

app自动化测试很重要?一文带你搞懂怎么测app

现在是移动互联网的时代,移动端APP产品在不断推陈出新,相应的APP测试人员也变得炙手可热。今天小濠跟大家全面谈谈APP应该怎么做测试。一、首先,移动测试要注意以下几点:移动APP测试中如何设计TestCaseÿ... 查看详情

回炉重造带你搞懂什么是机器学习?(代码片段)

带你搞懂什么是机器学习?机器学习介绍什么是机器学习:机器学习应用案例:机器学习程序开发流程:1.获取数据:2.清洗数据:3.训练模型:4.测试模型:5.投入使用:开发环境:Scikit-Learn... 查看详情

回炉重造带你搞懂什么是机器学习?(代码片段)

带你搞懂什么是机器学习?机器学习介绍什么是机器学习:机器学习应用案例:机器学习程序开发流程:1.获取数据:2.清洗数据:3.训练模型:4.测试模型:5.投入使用:开发环境:Scikit-Learn... 查看详情

一篇文章带你搞懂java注解的原理(代码片段)

文章目录一、注解的本质二、元注解三、Java内置的三大注解四、注解与反射五、总结一、注解的本质注解的本质就是一个继承了Annotation接口的接口例如:JDK内置注解的定义:@Target(ElementType.METHOD)@Retention(RetentionPolic... 查看详情

带你搞懂指针和数组方面的相关例题(代码片段)

引言要搞懂下面这些例题,首先你要知道关于数组名的意义以下这几点:数组名的意义:sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。&数组名,这里的数组名表示整个数组ÿ... 查看详情

一篇带你搞懂java集合(代码片段)

一、前言集合是java的基础。我们有了集合,在我们开发过程中,事半功倍。我们常用的集合有这几类:Array,Map,Set,Queue等,他们每一类在java迭代升级的过程中,也是有不同的升级优化。二、集... 查看详情