内存泄漏
内存管理
内存模型
Android原生开发以java为主。
在java中,Java内存模型,往往是指Java程序在运行时内存的模型,而Java代码是运行在Java虚拟机之上的,所以Java内存模型,也就是指Java虚拟机的运行时内存模型。
java中内存全权交给虚拟机去管理,那虚拟机的运行时内存是如何构成的?
很多时候,我们提到内存,会说到堆和栈,这是对内存粗略的一种划分,这种划分的”堆”对应内存模型的Java堆,”栈”是指虚拟机栈,但是实际上Java内存模型比这复杂多了。
在曾经的日公司(sun 已被甲骨文2009年收购) 制定的java虚拟机规范中,运行时内存模型,分为线程私有和共享数据区两大类,其中线程私有的数据区包含程序计数器、虚拟机栈、本地方法区,所有线程共享的数据区包含Java堆、方法区,在方法区内有一个常量池。
2.1 程序计数器PC
程序计数器PC是一块较小的内存空间,可以看作所执行字节码的行号指示器。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,比如循环、跳转、异常处理等等这些基础功能都需要依赖这个计数器来完成。
在开发多线程应用时,由于Java中的多线程是抢占式的调用,也就是任何一个确定的时刻,cpu都只会执行一条线程,执行哪条线程也是不确定的。所以为了线程切换后能够回到正确的执行位置,每个线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,所以这块区域是”线程私有”的内存。
当线程正在执行一个Java方法时,PC计数器记录的是正在执行的虚拟机字节码的地址;当线程正在执行的一个Native方法时,PC计数器则为空(Undefined)。这一块的内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。
2.2 虚拟机栈
和程序计数器一样,虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型。
每个方法(不包含native方法)执行的同时都会创建一个栈帧 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
我们平时所说的栈内存就是指的这一块区域。
Java虚拟机规范规定该区域有两种异常:
StackOverFlowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出 (递归函数)
OutOfMemoryError:当Java虚拟机动态扩展到无法申请足够内存时抛出(OOM)
2.3 本地方法栈
本地方法栈和虚拟机栈差不多,前者是为虚拟机使用到的Native方法提供内存空间。有些虚拟机的实现直接把本地方法栈和虚拟机栈合二为一,比如主流的HotSpot虚拟机。
异常(Exception):Java虚拟机规范规定该区域可抛出StackOverFlowError和OutOfMemoryError。
2.4 Java堆
Java堆,是Java虚拟机管理的最大的一块内存,也是GC的主战场,所以可以叫它gc堆(垃圾堆),里面存放的是几乎所有的对象实例和数组数据。
异常(Exception):Java虚拟机规范规定该区域可抛出OutOfMemoryError。
2.5 方法区
方法区主要存放的是已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。Java虚拟机规范对这一块区域的限制非常宽松,不同的虚拟机实现也不同,相对而言垃圾回收在这个区域比较少的出现。根据java虚拟机规范,当方法区无法满足内存分配需求时,会抛出oom异常。
2.6 运行时常量池
运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。运行时常量池除了编译期产生的Class文件的常量池,还可以在运行期间,将新的常量加入常量池,比较String类的intern()方法。
字面量:与Java语言层面的常量概念相近,包含文本字符串、声明为final的常量值等。
符号引用:编译语言层面的概念,包括以下3类:
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
属于方法区一部分,所以和方法区一样,会oom。
局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。
——因为它们属于方法中的变量,生命周期随方法而结束。
成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)
——因为它们属于类,类对象终究是要被new出来使用的。
我们说的内存泄露,是针对,也只针对堆内存,他们存放的就是引用指向的对象实体。
内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,java程序员不需要通过调用函数来释放内存,但gc只能回收无用并且不再被其它对象引用的那些对象所占用的空间。
堆中几乎存放着Java世界中所有的对象实例,垃圾收集器在对堆回收之前,第一件事情就是要确定这些对象哪些还“存活”着,哪些对象已经“死去”(即不可能再被任何途径使用的对象)
确定对象是否活着的方法有:
1、引用计数算法
1.1算法分析
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
1.2优缺点
优点:
引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:
无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.
1引用计数算法无法解决循环引用问题,例如:
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
}
}
最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。
2、可达性分析算法(主流方法)
可达性分析算法中,通过一系列的gc root为起始点,从一个GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
java中可作为GC Root的对象有
1.虚拟机栈(本地变量表)中正在运行使用的引用
2.方法区中静态属性引用的对象
3. 方法区中常量引用的对象
4.本地方法栈JNI中引用的对象(Native对象)
上图中objD与objE到GC ROOT不可达,所以可以被回收。而其他的对gc root可达。
在代码看来,类似于:(GC/Main.java)
但是即使在可达性分析算法中不可达的对象,也并非一定要死。当gc第一次扫过这些对象的时候,他们处于“死缓”的阶段。要真正执行死刑,至少需要经过两次标记过程。如果对象经过可达性分析之后发现没有与GC Roots相关联的引用链,那他会被第一次标记,并经历一次筛选。这个对象的finalize方法会被执行。如果对象没有覆盖finalize或者已经被执行过了。虚拟机也不会去执行finalize方法。Finalize是对象逃狱的最后一次机会。
Reference项目的FinalizeEscapeGC
在例子中,对象第一次被执行了finalize方法,但是把自己上交给国家逃了一死,但是在给国家执行任务的时候,不幸牺牲了。所以没办法再自救了。
这个对象的finalize方法执行了一次(自救而不是被救,this赋值,所以给的还是自己)
内存泄漏
在说到内存的问题,我们都会提到一个关键词:引用。
通俗的讲,通过A能调用并访问到B,那就说明A持有B的引用,或A就是B的引用。
比如 Person p1 = new
Person();通过P1能操作Person对象,因此P1是Person的引用;p1是类O中的一个成员变量,因此我们可以使用o.p1的方式来访问Person类对象的成员,因此o持有一个Person对象的引用。
GC过程与对象的引用类型是密切相关的,
Java对引用的分类Strong
reference(强), SoftReference(软), WeakReference(弱), PhatomReference(虚)
强引用就是在程序代码中普遍存在的,比如”Object obj = new Object()”这种引用,只要强引用还在,垃圾收集器就不会回收被引用的对象。
软引用用来定义一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要内存溢出之前,会将这些对象列入回收范围进行第二次回收,如果回收后还是内存不足,才会抛出内存溢出。
弱引用也是用来描述非必须对象。但他的强度比软引用更弱一些。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器回收时,无论内存是否足够,都会回收掉被弱引用关联的对象。
虚引用也称为幽灵引用或者幻影引用,是最弱的引用关系。一个对象的虚引用根本不影响其生存时间,也不能通过虚引用获得一个对象实例。虚引用的唯一作用就是这个对象被GC时可以收到一条系统通知。
在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且生命周期较长的对象时候,可以尽量应用软引用和弱引用技术。
对于软引用和弱引用的选择,
如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。
内存泄漏就是
堆内存中的长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的根本原因。
总结一句话就是不需要了该回收因为引用问题导致不能回收。
内存泄漏会导致可用内存慢慢变少,让程序慢慢变卡。最终还会导致臭名昭著的oom 内存溢出。
我们的android程序应该怎么排查内存泄漏问题呢?
在android中我们执行一段代码,比如进入了一个新的页面(Activity),这时候我们的内存使用肯定比在前一个页面大,而在界面finish返回后,如果内存没有回落,那么很有可能就是出现了内存泄漏。
从内存监控工具中观察内存曲线,是否存在不断上升的趋势且不会在程序返回时明显回落。这种方式可以发现最基本,也是最明显的内存泄露问题,对用户价值最大,操作难度小,性价比极高。
因为他能发现很明显很严重的内存泄漏问题
我们可以通过AS的Memory Profile或者DDMS中的heap观察内存使用情况。
Android Profile
https://developer.android.com/studio/preview/features/android-profiler.html
The newAndroid
Profilerwindow in Android Studio 3.0 replaces theAndroid Monitortools.
在Android Studio我们可以点击
运行app。
然后我们能看到
我们点击memory后
① 强制执行垃圾收集事件的按钮。
② 捕获堆转储的按钮。
③ 记录内存分配的按钮。
④ 放大时间线的按钮。
⑤ 跳转到实时内存数据的按钮。
⑥ 事件时间线显示活动状态、用户输入事件和屏幕旋转事件。
⑦ 内存使用时间表,其中包括以下内容:
每个内存类别使用多少内存的堆栈图,如左边的y轴和顶部的颜色键所示。
虚线表示已分配对象的数量,如右侧y轴所示。
每个垃圾收集事件的图标。
与之前的Android监控工具相比,新的内存分析器记录了更多内存使用情况,所以看起来你的内存使用量会更高。内存分析器监视一些额外的类别,这些类别增加了总数。
在比较茫然的情况下,不知道哪儿出现了内存泄漏,我们可以进行一刀切,来个大致的排查。
我们进入了一大堆页面并最终返回到主页。
然后gc ,再dump下内存查看。AS可以点击
然后等待一段时间会出现:
但是说实话这个页面想要分析出什么很难。一般不会使用这个页面来进行分析,最多打开看一眼刚刚我们进入的Activity是否因为我们退出而回收。
先按照包名来分组,
Alloc Cout : 对象数
Shallow Size : 对象占用内存大小
Retained Set : 对象引用组占用内存大小(包含了这个对象引用的其他对象)
当然一次dump可能并不能发现内存泄漏,可能每次我们dump的结果都不同,那么就需要多试几次,然后结合代码来排查。
这里还不能确定发生了内存泄漏。
我们这时候可以借助一些更专业的工具来进行内存的分析。
我们先把
这个内存快照保存为hprof文件。
AS自动分析
其实现在的AS,可以说是非常强大了。我们把刚刚保存的hprof文件拖入到AS中。
这个自动分析任务包含了两个内容,一个是检测Activity的泄漏,一个是检测重复字符串。
点击运行分析:
这里出现了MainActivity的泄漏。并且观察到这个MainActivity可能不止一个对象存在,可能是我们上次退出程序的时候发生了泄漏,导致它不能回收。而在此打开app,系统会创建新的MainActivity。
但是在AS上发现为何没被回收需要运气,更方便的工具是Mat。
Memory Analyzer Tool基于eclipse
可以直接下载:
http://www.eclipse.org/mat/downloads.php
也可以在eclispe上安装mat插件:
点击eclipse marketplace...搜索memory。
在使用mat之前我们需要把快照文件转换一下,
转换工具在sdk/platform-tools/hprof-conv
-z:排除不是app的内存,比如Zygote
hprof-conv -z src dst
然后在Mat中打开它:
打开我们的快照文件,
之后我们能看到
我们点击
以直方图的方式来显示当前内存使用情况可能更加适合较为复杂的内存泄漏分析,它默认直接显示当前内存中各种类型对象的数量及这些对象的shallow heap和retained heap。结合MAT提供的不同显示方式,往往能够直接定位问题
shallow heap:指的是某一个对象所占内存大小。
retained heap:指的是一个对象与所包含对象所占内存的总大小。
out查看这个对象持有的外部对象引用。
incoming查看这个对象被哪些外部对象引用。
我们现在希望查看为什么这个对象还存在,那么
排除软弱虚引用。
关于这个问题是android系统的一个bug。
原因是Activity的DecorView请求了InputMethodManager,而InputMethodManager一直持有DecorView的引用,导致无法释放Activity。
解决办法是:
这个问题是一个第三方库中持有引用,导致的无法释放。
这个问题可能是这个第三方库本身的问题,也可能是我们使用不当引起的。为了确定这个问题。我们进入被混淆了的g.e这个类,
注意这里是g.e中的a成员是一个HashMap,
结合代码
这里面保存的都是一些Observer对象。这里就需要结合对代码的熟悉度,加上一些猜测来寻找问题。
在Activity中搜索Observer,可以找到很多个new Observer。
但是我们注意,调用的地方第二个参数是true,表示注册;如果传false表示注销
而这里只有注册,没有注销。
我们在onDestory中加入注销。
修改完成再次运行,然后退出app一次再打开:
结果:
还有两个Activity。
第一个问题仍然是observer,但是是在MessageFragment。
第二个问题也还是InputMethodmanager,但是泄漏点变了。
修改:
内存泄漏解决完成!
这个app还有很多内存泄漏的地方。大家可以自己尝试去寻找并解决。比如MyInfoActivity。。。。大家自己去解决啊,有什么不懂得再问我。如果问的人多,下节课先解决掉这个泄漏。
除了检查单个hprof文件之外,还能够使用多个hprof进行对比。
比如我们在进入一个新页面之前先dump下来内存。然后再进入这个页面之后退出,再dump一份内存。通过对比就能够知道,进入这个页面之后增加了多少内存/对象等信息:
之后在mat中打开这个两个文件并都切换到直方图查看。
再然后把两个直方均加入对比,点击
或者
执行对比:
再把视图切换到difference from base table(与第一个的不同)
然后能看到
第二行就是指第二个文件相比第一个文件多出来了几个对象。
如果存在增加了不合理的对象,同样可以查看其GC root。
=====================================================================
对Android内存泄露 我们还可以使用著名的LeakCanary
(Square出品,Square可谓Android开源界中的业界良心,开源的项目包括okhttp, retrofit,otto, picasso, Android开发大神Jake Wharton曾今就是Square)来进行检测
https://github.com/square/leakcanary
这个库也有一些bug,但总体来说还是能起到一定的辅助作用。
总结:
内存泄漏常见原因:
1.集合类
集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。
一开始的微信工程中使用的ButterKnife中的linkeahashmap就存在这个问题。
2、静态成员
Static成员作为gc root,如果一个对象被static声明,这个对象会一直存活直到程序进程停止。
2.单例模式
不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被 JVM 正常回收,导致内存泄露。
这里如果传递Activity作为Context来获得单例对象,那么单例持有Activity的引用,导致Activity不能被释放。
不要直接对 Activity 进行直接引用作为成员变量,如果允许可以使用Application。如果不得不需要Activity作为Context,可以使用弱引用WeakReference,相同的,对于Service 等其他有自己声明周期的对象来说,直接引用都需要谨慎考虑是否会存在内存泄露的可能。
3.未关闭/释放资源
BraodcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity
onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。
我们经常会写出下面的代码
当然这样写代码没问题,但是如果我们在close之前还有一些可能抛出异常的代码
那么现在这段代码存在隐患的。因为如果运行到fos2时候抛出了异常,那么fos也没办法close。
所以正确的方式应该是
因为如果write发生异常那么这个fos会因为没有close造成内存泄漏。
4.
Handler
只要 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。特别是handler执行延迟任务。所以,Handler 的使用要尤为小心,否则将很容易导致内存泄露的发生。
这种创建Handler的方式会造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏,所以另外一种做法为:
创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息,
使用mHandler.removeCallbacksAndMessages(null);是移除消息队列中所有消息和所有的Runnable。当然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();来移除指定的Runnable和Message。
5.
Thread 内存泄露
和handler一样,线程也是造成内存泄露的一个重要的源头。线程产生内存泄露的主要原因在于线程生命周期的不可控。比如线程是 Activity 的内部类,则线程对象中保存了 Activity 的一个引用,当线程的 run 函数耗时较长没有结束时,线程对象是不会被销毁的,因此它所引用的老的 Activity 也不会被销毁,因此就出现了内存泄露的问题。
Thread和Handler都可以划分到为非静态包括匿名内部类的内存泄漏。
6、系统bug,比如InputMethodManager