目录
JVM工作原理
运行时数据区
垃圾回收(Garbage Collection)
如何判断对象为垃圾对象?
垃圾回收算法
四种引用类型
什么是内存泄露
为什么会有内存泄露
Android 中导致内存泄漏的常见场景
检查策略
实例分析
内部类概念
要想知道Android App内存泄漏的根本原因,首先要了解Java虚拟机(Java Virtual Machine)的工作原理。
JVM工作原理
编写好的java源程序,通过java编译器javac编译成java虚拟机识别的class文件(字节码文件),然后由JVM中类加载器加载字节码文件,加载完毕之后再由JVM引擎去执行。
在加载完毕到执行过程中,JVM会将程序执行时用到的数据和相关信息存储在运行时数据区(Runtime Data Area),这块区域就是常说的JVM内存结构,垃圾回收也是作用在这块区域。
程序计数器(Program Computer Register)
是块较小的内存空间,是当前线程所查找行的字节码的行号指示器,在虚拟机的概念模型里,字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
Java虚拟机栈(Java Virtual Machine Stack)
这块区域是线程私有的,与线程同时创建,用于存储栈帧。java每个方法执行的时候都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
本地方法栈(Native Method Stacks)
作用和虚拟机栈类似,虚拟机栈执行的是java方法,本地方法栈执行的是Native方法,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
Java堆
java堆是java虚拟机所管理内存最大、被所有线程共享的一块区域,目的是用来存放对象,基本上所有的对象实例和数组都在堆上分配(不是绝对)。java堆是垃圾回收器管理的主要区域。
方法区(Method Area)
用来存储已被java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码数据。方法区也称为“永久代”,这是因为垃圾回收器对方法区的垃圾回收比较少,主要是针对常量池的回收以及对类型的卸载,回收条件比较苛刻。对此内存未完全回收而导致的内存泄漏,最后当方法区无法满足内存分配时,将抛出OutOfMemoryError异常。
Java GC 实质上也就是一个运行在Java虚拟机(JVM)上的一个程序,它自动地管理着内存的使用,在适当的时机释放并回收无用的内存分配。
Java内存回收机制主要作用在Java堆和方法区。
引用计数算法
给每一个创建的对象增加一个引用计数器,每当一个地方引用它时,这个计数器就加1;而当引用失效时,这个计数器就减1.当这个引用计数器值为0时,也就是说这个对象没有任何地方在使用它了,那么就是一个无效的对象,便可以进行垃圾回收了。
缺点:无法解决对象之间循环引用的问题。
根搜索算法
在主流的商用程序中,都是使用根搜索算法(GC Roots Tracing)来判定对象是否存活。
算法:通过一系列为“GC Roots”的对象作为终点,当一个对象到GC Roots之间无法通过引用到达时,那么该对象便可以进行回收了。
在java语言中,有4种对象可以作为GC Roots:
-
栈变量:虚拟机栈(栈帧中的本地变量表)中引用的对象
-
静态变量:方法区中的静态变量属性引用的对象
-
常量池:方法区中常量引用的对象
-
JNI:本地方法栈中(JNI)(Native方法)的应用对象
标记-清除算法
实现:分为标记和清除两个阶段,首先根据上面的根搜索算法标记出所有需要回收的对象,在标记完成后,然后统一回收掉所有被标记的对象。
缺点:
- 效率低:标记和清除这两个过程的效率都不高
- 容易产生内存碎片:因为内存的申请通常不是连续的,那么清除一些对象后,那么就会产生大量不连续的内存碎片,而碎片太多时,当有个大对象需要分配内存时,便会造成没有足够的连续内存分配而提前触发垃圾回收,甚至直接抛出OutOfMemoryException。
复制算法
实现:将可用内存按容量划分为大小相等的两块区域,每次只使用其中一块,当这一块内存用完了,就将还活着的对象复制到领一块区域上,然后再把已使用过的内存空间一次性清理掉。
优点:
- 每次都是对其中一块内存进行回收,不用考虑内存碎片的问题,而内存分配时,只需要移动堆顶指针,按顺序进行分配即可,简单高效。
缺点:
- 将内存分为两块,但是每次只能使用一块,即机器的一半内存是闲置的,资源严重浪费。并且如果对象存活率较高,每次都需要复制大量对象,效率也会变得很低。
标记-整理算法
实现:首先标记出所有存活的对象,然后让所有存活对象向一端进行移动,最后直接清理边界以外的内存。
局限性:只有对象存活率很高的情况下,使用该算法才会效率较高。
分代收集算法
实现:根据对象的存活周期不同将内存分为几块,然后不同区域采用不同的回收算法。
- 对于存活周期较短,每次都有大批对象死忙,只有少量存活的区域,采用复制算法,因为只需要付出少量存活对象的复制成本即可完成收集。
- 对于存活周期较长,没有额外空间进行分配担保的区域,采用标记-整理算法,或标记-清除算法。
- 堆由新生代和老年代两块区域组成,而新生代区域又分为三个部分:Eden、From Survivor、To Survivor,比例是8:1:1.
- 新生代采用复制算法,每次使用一块Eden区和一块Survivor区,当进行垃圾回收时,将Eden和一块Survivor区域的所有存活对象复制到另一块Survivor区域,然后清理掉之前存放对象的区域,依次循环。
- 老年代采用标记-清除或标记-整理算法,根据使用的垃圾回收器来判断。
-
强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;
-
软引用(SoftReference):只有在内存空间不足时,才会被回的对象;
-
弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
-
虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。
简单来说就是:不应该被 GC Roots 访问到到的内存,仍能被访问到,GC Roots 误以为这块内存区域并不是垃圾,导致该回收的内存没被回收。
久而久之,内存泄露越来越严重,旧的垃圾内存得不到回收,新的垃圾内存不断增加,可用的内存也就越来越少。
JVM 为了申请新的内存空间,频繁触发 GC,程序执行效率将会受到影响,程序甚至直接抛出 Out Of Memory Exception 异常退出。
obj2 对象引用 obj1 对象,obj2 的生命周期(t1-t4)比 obj1 的生命周期(t2-t3)长的多。当 obj1 没有被应用程序使用之后,obj2 仍然在引用着 obj1 ,这样,垃圾回收器就没办法将 obj1 对象从内存中移除,从而导致内存泄漏问题。
原因 |
预防措施 |
资源释放问题:如 Cursor、IO 流的引用,资源得不到释放造成内存泄露 |
Cursor、IO流在使用完毕后及时close |
Context引用问题:耗时线程持有普通Context |
1.尽量使用 ApplicationContext , 因为 Application 的 Context 的生命周期比较长; 2.使 用 WeakReference 代 替 强 引 用 。比 如 可 以 使 用 WeakReference<Context> mContextRef; |
static 关键字的使用问题:static 修饰的变量,它的生命周期与类一样 |
尽量避免 static 成员变量引用资源耗费过多的实例, 比如 Context,View。 |
非静态内部类:非静态内部类持有外部类的实例对象,如果内部类生命周期长于外部类就会导致外部类无法回收,造成泄漏,容易出现问题的有Handler,Runnable,Thread等 |
1.将内部类,改为静态内部类 2.在内部类内采用弱引用保存Context 引用。 |
图片过大导致OOM |
1.等比例缩小图片 2.对图片采用软引用,及时地进行 recyle()操作 3.使用加载图片框架处理图片 |
构造Adapter 时,没有使用缓存的 convertView |
尽量使用RecyclerView代替ListView |
Activity结束时资源没有释放:如BroadcastReceiver,各类监听器 |
在 onPause()、onStop()、 onDestroy()方法中需要注意释放资源的情况 |
WebView造成的泄露 |
不再使用WebView对象时,调用它的destory()函数来销毁它 |
- 可根据细分原因逐一检查,重点检查Handler,Runnable,Thread等的使用。
- 可使用LeakCanary进行检查。
LeakCanary使用方法:
- Gradle的dependencies中添加 debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.7’
- 进入各页面,然后退出
- adb shell am start -n “包名/leakcanary.internal.activity.LeakLauncherActivity"
- 确认是否有内存泄漏情况,如果有根据给出的信息进行确认
分析:从trace的最后记录看,为FrameLayout不能回收,即Toast.mNextView没有释放,再向上可以看到是ToastUtil.sToast是static变量
解决:在Toast显示之后设置sToast.setView(null)
分析:从trace最后看,是ConstraintLayout无法释放,原因是DataBindingImpl的root还持有引用,再向上为mBinding持有引用
解决:在Fragment的onDestroyView时设置mBinding = null
-
Callback,Runnable等引起的内存泄漏
分析:从trace最后看为MessageViewModel没有被释放,原因往上看是有与MessageListCallback持有其引用。
分析:从代码看,是使用了匿名内部类,默认持有外部类引用
解决:修改为静态内部类
分析:从trace最后记录看,CatalogFragment退出后没有被释放,往上看是由于LoadingAdapter持有了mBinding,而Loading又持有了LoadingAdapter,Loading是个单例,sDefault是静态变量。
解决:在Fragment的onDestroyView方法中重置Loading的adapter
在 Java 语言中,内部类是指在一个外部类的内部再定义一个类。而对于这个内部类来说,它可以是静态 static 的,也可以用(访问修饰符)public,protected,default 和 private 来修饰。(而包含这个内部类的外部类只能使用 public 和 default来进行修饰)。
而在字节码语言中,只有类的概念,没有外部类和内部类的概念,类只能使用 public 和 default 进行访问控制。
在外部类的内部,定义的非静态的内部类,叫成员内部类
在外部类的内部,定义的静态的内部类,叫静态内部类(或叫嵌套类)。
①在外部类的实例方法内部的局部内部类;
②在外部类的静态方法内部的局部内部类。