美文网首页
[Android] 内存泄漏分析及其解决方法,有这一篇就足矣

[Android] 内存泄漏分析及其解决方法,有这一篇就足矣

作者: 小海绵Ya | 来源:发表于2018-07-21 17:41 被阅读0次

内存泄漏的本质是本该被回收的对象因为被引用所以未能被回收。手机分配给每个APP的内存是固定的,要是内存占用过大就会导致APP卡顿。JVM即Java Virtual Machine(Java虚拟机)会对堆中的对象进行监控,回收没被引用的对象,这个过程叫做GC(Garbage Collection,垃圾收集,垃圾回收)。

GC工作原理

JVM存储管理

Java采用GC进行内存管理

Android虚拟机的垃圾回收采用的是根搜索算法。GC会从根节点(GC Roots)开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉,这就导致了内存泄漏。

内存垃圾回收机制

是从程序的主要运行对象(如静态对象/寄存器/栈上指向的堆内存对象等)开始检查引用链,当遍历一遍后得到上述这些无法回收的对象和他们所引用的对象链,组成无法回收的对象集合,而其他孤立对象(集)就作为垃圾回收

GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

关于GC的调用

有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能使用不同的算法管理GC

通常GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。

如何监听GC过程

系统每进行一次GC操作时,都会在LogCat中打印一条日志,我们只要去分析这条日志就可以了,日志的基本格式如下

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <Pause_time>

1、第一部分GC_Reason,这个是触发这次GC操作的
一般情况下一共有以下几种触发GC操作的原因:
GC_CONCURRENT: 当我们应用程序的堆内存快要满的时候,系统会自动触发GC操作来释放内存。
GC_FOR_MALLOC: 当我们的应用程序需要分配更多内存,可是现有内存已经不足的时候,系统会进行GC操作来释放内存。
GC_HPROF_DUMP_HEAP: 当生成HPROF文件的时候,系统会进行GC操作,关于HPROF文件我们下面会讲到。
GC_EXPLICIT: 这种情况就是我们刚才提到过的,主动通知系统去进行GC操作,比如调用System.gc()方法来通知系统。或者在DDMS中,通过工具按钮也是可以显式地告诉系统进行GC操作的。
2、第二部分Amount_freed,表示系统通过这次GC操作释放了多少内存
3、第三部分Heap_stats中会显示当前内存的空闲比例以及使用情况(活动对象所占内存 / 当前程序总内存)
4、第四部分Pause_time表示这次GC操作导致应用程序暂停的时间。关于这个暂停的时间,Android在2.3的版本当中进行过一次优化,在2.3之前GC操作是不能并发进行的,也就是系统正在进行GC,那么应用程序就只能阻塞住等待GC结束。虽说这个阻塞的过程并不会很长,也就是几百毫秒,但是用户在使用我们的程序时还是有可能会感觉到略微的卡顿。而自2.3之后,GC操作改成了并发的方式进行,就是说GC的过程中不会影响到应用程序的正常运行,但是在GC操作的开始和结束的时候会短暂阻塞一段时间,不过优化到这种程度,用户已经是完全无法察觉到了

GC过程与对象的引用类型关系

Java对引用的分类Strong reference, SoftReference, WeakReference, PhatomReference

在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术

软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

JVM的内存分配策略

静态存储区:内存在程序编译的时候就已经分配好了,生命周期和整个程序生命周期一样。这块区域用来存储静态变量、全局常量。

栈区:提供一块连续的内存区域来存储引用变量、局部变量,不会碎片化。因为它在处理器里面,所以运算速度很快。

堆区:提供一块不连续的存储区域来存储实例化的对象,包括所有的成员变量(基本数据类型、引用和引用对象)。容易碎片化。举个例子:


内存泄漏和内存溢出的区别

内存泄漏是有对象本该被GC回收却因为被引用没有被回收;内存溢出是指heap资源已经超过了JVM分配的内存,是内存泄漏严重到一定程度产生的后果。

典型内存泄漏案例

循环创建对象产生内存泄漏

循环获取Object类型的实例对象,虽然最后赋值为null,但v依然持有实例对象的引用,导致实例对象不能被GC回收。解决方法是最后将v清除掉,将v赋值为null最便捷。

Handler中的内存泄漏

申请了一个延迟4秒钟的Message,在Acticity被finish的时候,Message依然持有Handler的引用,而Handler又持有Activity的引用,导致Activity不能被回收,因此内存泄漏。解决方法是用static去声明Handler,使之成为静态内部类,静态内部类默认不持有Activity的引用。但有种情况需要注意,用到 Context 等外部类的 非static 对象,还是应该通过弱引用传入。比如:

为了防止内存泄漏,推荐使用静态内部类+弱引用 WeakReference 这种方式,但要注意每次使用前进行判空。

注意:虽然避免了内存泄漏,但Looper线程的消息队列中可能还有未处理的消息,因此在onDestory和Onstop中清除消息。

总结一下防止内存泄漏的措施

1、构造 Adapter 时,使用缓存的 convertView

2、Bitmap 对象调用 recycle() 释放内存

3、尽量使用应用 ApplicationContext 代替 Context 的地方进行替换,这样不会导致Activity被引用而不能被回收。

4、内部类尽量使用静态内部类,静态内部类默认不会引用外部类,当需要引入外部类的时候,则尽量使用静态类和弱引用来处理(譬如Handler+Activity+Message的实现)。

5、警惕线程未终止造成的内存泄露;譬如在 Activity 中关联了一个生命周期超过 Activity 的 Thread,在退出 Activity 时切记结束线程。一个典型的例子就是 HandlerThread 的 run 方法是一个死循环,它不会自己结束,线程的生命周期超过了 Activity 生命周期,我们必须手动在Activity的销毁方法中中调运 thread.getLooper().quit(); 才不会泄露。

6、广播的注册、注销要成对出现。

7、创建与关闭没有成对出现造成的泄露;譬如 Cursor 资源必须手动关闭,WebView 必须手动销毁,流等对象必须手动关闭等。

检测内存泄漏的工具

1、 MAT(Memory Analyzer Tool),下载地址:
http://www.eclipse.org/mat/downloads.php
2、 强大的开源内存检测工具 LeakCanary。
leakcanary 是一个开源项目,一个内存泄露自动检测工具,是著名的 GitHub 开源组织 Square 贡献的,它的主要优势就在于自动化过早的发觉内存泄露、配置简单、抓取贴心,其核心原理与MAT工具类似。

相关文章

网友评论

      本文标题:[Android] 内存泄漏分析及其解决方法,有这一篇就足矣

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