我的CSDN: ListerCi
我的简书: 东方未曦
一、引言
一般情况下Android的内存泄漏是因为,存在引用指向一个本该被回收的对象,例如已经执行onDestroy()的Activity。在这种情况下,由于Activity内某些对象的生命周期比Activity要长,在Activity理论上被销毁时,该对象依旧存在并持有Activity的引用,因此内存回收机制(GC)无法释放Activity,最终导致内存泄漏。
为了发现和修复APP中存在的内存泄漏,开发人员会在APP上安装内存检测工具(如leakcanary),当出现内存泄漏时,该工具会提供一个报告,里面包含了一条引用链,指明可能造成内存泄漏的引用。开发人员需要在合适的地方切断引用链,以便GC释放掉没有被引用的对象。
有些内存泄漏的修复很简单,将非静态内部类内部类改为静态内部类或者将Context改为ApplicationContext后检测工具就检测不出内存泄漏了,但是这到底是为什么呢?而且就算检测工具检测不出内存泄漏,就真的万无一失了吗?
带着这些问题,我们来分析一下Android常见的内存泄漏场景以及解决方案。
二、Java内存管理及垃圾回收机制
在了解Android的内存泄漏之前,我们需要先了解Java的内存管理以及垃圾回收机制。
2.1 内存管理
Java的内存分配区域主要分为以下几个部分。
1. 静态变量区
用于存储被static修饰的静态变量,这块区域在程序开始运行时就已经分配完毕,并且存在于程序的整个运行过程。
2. 栈
主要用于分配局部变量,包括基本类型的变量和对象的引用变量,当局部变量的作用域结束之后,Java会自动释放掉该变量占用的内存空间。
3. 堆
堆是动态内存区域,程序运行期间新建的对象实例和数组都存储在堆中,垃圾回收机制(GC)管理的就是这块内存。为了及时地将不被使用的对象释放掉,GC需要监控每一个对象的状态,当一个对象不再被引用时,GC就会释放该对象。
4. 常量池
常量池中的内容在编译时就已经确定,主要包含代码中的基本类型和对象类型的常量值。
例如,String就是对象类型,如果在编译时确定了String的值(String s = "test"),那么它的值就存储在常量池中,而它的引用存储在栈中。如果String的值是在程序运行时确定的(String s = new String("...")),那么它的值就存储在堆中。
假设当前有一个实例A存储在堆中,我们定义了一个引用a指向实例A。此时引用a其实是保存在栈中的,它的值为实例A在堆内存中的首地址,此时程序就可以通过a读写A的值。
2.2 垃圾回收机制
上面提到,当一个对象不再被引用时,GC就应该将其回收。确实有一种引用计数法来判断一个对象是否需要被释放,当该对象的引用计数为0时代表它需要被回收。但是如果存在两个对象,没有别的引用指向它们,但是它们互相引用,此时它们的引用计数都不为0,导致无法释放,容易造成内存泄漏。
目前主流的的方法是通过可达性分析来判断一个对象是否需要被释放。该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。
在Java中,可作为GC Root的对象包括以下几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
三、Android常见内存泄漏场