美文网首页android性能
Android 内存泄漏分析和案例讲解

Android 内存泄漏分析和案例讲解

作者: 搬砖写Bug | 来源:发表于2018-09-17 20:40 被阅读104次

一、概述

1. Java GC 的概念

GC 即为 Garbage Collection,垃圾回收机制。

Java GC 实质上也就是一个运行在Java虚拟机(JVM)上的一个程序,它自动地管理着内存的使用,在适当的时机释放并回收无用的内存分配。

使得我们不用像写 C++ 那样手动释放内存,从而帮助我们释放双手。那它是如何知道哪些内存分配是无用的,而哪些是有用的呢?

下面是 Google 的一张图:

2. 什么是内存泄露

简单来说就是:不应该被 GC Roots 访问到到的内存,仍能被访问到,GC Roots 误以为这块内存区域并不是垃圾,导致该回收的内存没被回收。

久而久之,内存泄露越来越严重,旧的垃圾内存得不到回收,新的垃圾内存不断增加,可用的内存也就越来越少。

JVM 为了申请新的内存空间,频繁触发 GC,程序执行效率将会受到影响,程序甚至直接抛出 Out Of Memory Exception 异常退出。

3. 为什么会有内存泄露

如上图所示, obj2 对象引用 obj1 对象,obj2 的生命周期(t1-t4)比 obj1 的生命周期(t2-t3)长的多。当 obj1 没有被应用程序使用之后,obj2 仍然在引用着 obj1 ,这样,垃圾回收器就没办法将 obj1 对象从内存中移除,从而导致内存泄漏问题。

如果 obj2 引用更多这样的对象,将会有更多的未被引用对象存在,消耗内存空间。

而且,obj1 也可能会持有许多其他的对象,那这些对象同样也不会被垃圾回收器回收,导致占用大量的内存空间。

二、Android 中导致内存泄漏的常见场景

1. 非静态内部类导致的内存泄漏

非静态内部类会隐式持有外部类的引用,这是 Java的特性导致的,如果内部类的对象长时间使用不销毁,就会导致外部类对象得不到释放。

MainActivity 启动时开启一个子线程下载文件,如果文件较大或网络不稳定的因素,导致短时间内无法执行完成,用户退出了当前界面,此时子线程仍然在运行,并持有 mHandler 的引用,而 mHandler 是一个匿名内部类的对象,持有 MainActivity 的引用,这样导致 MainActivity 对象无法被回收,被MainActivity 内部持有的很多资源也都无法被回收。

解决方案:
将 Handler 声明为静态内部类,因为静态内部类不会持有外部类的引用,所以不会导致外部类实例出现内存泄露,如果需要有场景需要访问外部 Activity 对象,可以在 Handler 中添加对外部 Activity 的弱引用,如下图:

2. 静态 View/Context 导致的内存泄漏

静态对象生命周期基本上是整个进程的生命周期长度,如果有直接或间接引用到 Activity,很可能会导致内存泄漏。

如上图,textView 会持有 Activity 的引用,而静态 view 的生命周期和类是一样长 ,这样导致 Activity 不能被回收。

3. 监听器导致内存泄漏

代码中很多场景会使用到注册监听器,比如广播、系统回调接口等,如果我们没有在恰当的时间点去注销这些监听器,很可能就会导致内存泄漏,目前工作中遇到大部分内存泄漏都是这类原因导致。

下面是 google 原生「设置模块」的一个监听器使用不当导致的 Bug,onAttached() 和 onDetached() 竟然都是调用 addListener(),导致DataSaverPreference 所在的 Activity 在退出后无法被回收,引起内存泄漏。

4. 资源没有释放导致内存泄漏

源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候应该及时关闭它们,以便它们的缓冲及时回收内存。
它们的缓冲不仅存在于 Java 虚拟机内,还存在于 Java 虚拟机外。如果我们仅仅是把它设为 null,而不关闭它们,往往会造成内存泄露。

这类问题比较少见,工作中遇到过 fd 泄漏导致的 crash,如下:

01-01 09:07:52.164  5162  6777 W zygote64: ashmem_create_region failed for 'indirect ref table': Too many open files
01-01 09:07:52.152  5162  5162 E AndroidRuntime: FATAL EXCEPTION: main
01-01 09:07:52.152  5162  5162 E AndroidRuntime: Process: com.android.settings, PID: 5162
01-01 09:07:52.152  5162  5162 E AndroidRuntime: java.lang.RuntimeException: Could not read input channel file descriptors from parcel.
01-01 09:07:52.152  5162  5162 E AndroidRuntime:    at android.view.InputChannel.nativeReadFromParcel(Native Method)
01-01 09:07:52.152  5162  5162 E AndroidRuntime:    at android.view.InputChannel.readFromParcel(InputChannel.java:148)
01-01 09:07:52.152  5162  5162 E AndroidRuntime:    at android.view.IWindowSession$Stub$Proxy.addToDisplay(IWindowSession.java:828)
01-01 09:07:52.152  5162  5162 E AndroidRuntime:    at android.view.ViewRootImpl.setView(ViewRootImpl.java:729)
01-01 09:07:52.152  5162  5162 E AndroidRuntime:    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:356)
01-01 09:07:52.152  5162  5162 E AndroidRuntime:    at android.view.WindowMana

sysinfo 通过 lsof 命令可以看到 fd 信息,从出问题最近的时间点来看, settings 进程出现大量 eventfd 没有被释放.

解决方案:
通过不断反复测试发现,是 SummaryLoader 中创建的 HanlerThread 没有及时 quit 导致 Handlerthread 越来越多,而 Handlerthread 的会创建 eventfd,最终导致 fd 泄漏,此问题的解决方案只需在界面退出时 onDestroy() 方法中调用 Handlerthread 的 quit() 方法即可。

5. JNI 调用导致测内存泄漏

JNI 规范中定义了三种引用:

  • 局部引用(Local Reference):通过 NewLocalRef 和各种JNI接口创建(FindClass、NewObject、GetObjectClass 和NewCharArray等),会阻止 GC 回收所引用的对象,不在本地函数中跨函数使用,不能跨线前使用。函数返回后局部引用所引用的对象会被 JVM 自动释放,或调用 (*env)->DeleteLocalRef(env,local_ref) 方法释放;

  • 全局引用(Global Reference):调用 NewGlobalRef 基于局部引用创建,会阻 GC 回收所引用的对象,可以跨方法、跨线程使用。JVM 不会自动释放,必须调用 (*env)->DeleteGlobalRef(env,g_cls_string) 手动释放;

  • 弱全局引用(Weak Global Reference):调用 NewWeakGlobalRef 基于局部引用或全局引用创建,不会阻止 GC 回收所引用的对象,可以跨方法、跨线程使用。引用不会自动释放,在 JVM 认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放,或调用 (*env)->DeleteWeakGlobalRef(env,g_cls_string) 手动释放。

从定义来看,全部引用对象在函数返回后必须手动释放,不然会有内存泄漏的风险,关于 JNI 三种引用详细介绍可以参考这篇博客,写得很详细:
JNI/NDK开发指南(十)——JNI局部引用、全局引用和弱全局引用

三、如何查看内存泄漏

1. 通过GC log查看当前进程内存使用情况

I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects,
21(416KB) LOS objects,33% free, 25MB/38MB, paused 1.230ms total 67.216ms

I/art: <GC触发原因> <GC名称> <释放对象个数>(<释放字节数>) AllocSpace Objects,<释放大对象个数>(<释放大对象字节数>) <堆统计> LOS objects, <暂停时间>
GC触发原因:Explicit
GC名称:concurrent mark sweep GC
释放对象个数:104710
释放字节数:7M
释放大对象个数:21
释放大对象字节数:416KB
堆统计:堆空闲内存为33%,已用内存:25M, 总内存总:38M
暂停时间:GC暂停时长:1.230ms,GC总时长:67.216ms

2. 通过 Android Debug Monitor 工具实时查看

十次横竖屏后:

建议:若单一操作反复进行,堆大小一直增加,则有内存泄露的隐患,可采用MAT进一步查看

3. 使用 MAT 工具分析内存泄漏

MAT 工具介绍可以参看这篇博客:Android 内存剖析 – 发现潜在问题

四、案例分析

1. 确认问题

从 log 看,crash 的原因是JNI ERROR (app bug): weak global reference table overflow(max=51200),这句话表明 JNI 层弱全局引用个数超出最大限制51200,抛出异常。

01-24 15:43:43.681 29266 29266 F DEBUG   : pid: 17464, tid: 17464, name: ndroid.settings  >>> com.android.settings <<< 
01-24 15:43:43.681 29266 29266 F DEBUG   : signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr -------- 
01-24 15:43:43.692 29266 29266 F DEBUG   : Abort message: 'art/runtime/indirect_reference_table.cc:125] JNI ERROR (app bug): weak global reference table overflow (max=51200)'

报错打印的调用栈:

01-24 15:43:43.188 17464 17464 E         :   at com.android.settingslib.drawer.SettingsDrawerActivity.onCreate(SettingsDrawerActivity.java:103)
01-24 15:43:43.188 17464 17464 E         :   at com.android.settings.SettingsActivit:
01-24 15:43:43.188 17464 17464 E         :   at android.view.RenderNode.nCreate(Native method)
01-24 15:43:43.188 17464 17464 E         :   at android.view.RenderNode.<init>(RenderNode.java:137)
01-24 15:43:43.188 17464 17464 E         :   at android.view.RenderNode.create(RenderNode.java:161)

再从问题时间点看 Settings heap 已经 144M,而且还在不断变大,这种情况就是发生内存泄漏了:

15:43:09.563 17464 17464 I art     : Explicit concurrent mark sweep GC freed 295(16KB) AllocSpace objec ts, 0(0B) LOS objects, 9% free, 144MB/160MB, paused 2.508ms total 864.394ms
15:43:10.407 17464 17464 I art     : Explicit concurrent mark sweep GC freed 156(6KB) AllocSpace objects, 0(0B) LOS objects, 9% free, 144MB/160MB, paused 2.110ms total 836.663ms

2. 查看内存泄漏点

确认是内存泄漏的问题后,就需要查找出哪里泄漏,查看内存泄漏一般需要 Hprof 文件,但是由于是 monkey 跑出来的没有生成 Hprof,所以很难定位,只能通过 event.log 查看操作过哪些界面尝试手动复现。
中间查找过程比较痛苦,需要一个一个尝试,最终发现如下信息:DataUsageSummaryActivity 这个实例启动 143 次:

13:35:04.278 17464 17464 I am_on_resume_called: [0,com.android.settings.Settings$DataUsageSummaryActivity,LAUNCH_ACTIVITY]
13:35:04.278 17464 17464 I am_on_resume_called: [0,com.android.settings.Settings$DataUsageSummaryActivity,LAUNCH_ACTIVITY]
13:35:53.686 17464 17464 I am_on_resume_called: [0,com.android.settings.Settings$DataUsageSummaryActivity,LAUNCH_ACTIVITY]
13:35:53.686 17464 17464 I am_on_resume_called: [0,com.android.settings.Settings$DataUsageSummaryActivity,LAUNCH_ACTIVITY]
13:36:05.603 17464 17464 I am_on_resume_called: [0,com.android.settings.Settings$DataUsageSummaryActivity,LAUNCH_ACTIVITY]
……
……
13:36:41.687 17464 17464 I am_on_resume_called: [0,com.android.settings.Settings$DataUsageSummaryActivity,LAUNCH_ACTIVITY]

如下是反复进出十次左右Settings堆栈大小
通过DDMS工具查看Settings占用内崔大小,刚开始 只有 9M 左右,反复十次操作后增大到 15M,而且无法回收:

抓取此时 Settings 进程的 Hprof 文件,使用
MAT工具查看,发现 DataSaverBackend 持有
DataUsageSummaryActivity 的引用导致内存泄漏:

到这里已经完成了大半,然后就去查看DataSaverBackend.java 和
DataSaverPreference.java 两个类的源代码,发现其中问题:

相关文章

网友评论

    本文标题:Android 内存泄漏分析和案例讲解

    本文链接:https://www.haomeiwen.com/subject/pytagftx.html