一、概述
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 两个类的源代码,发现其中问题:
网友评论