一 什么是内存泄漏
【定义】内存泄漏(Memory Leak),是指程序申请内存后,当该内存不再需要,但无法被释放的现象
【影响】容易造成应用发生内存溢出(OOM-Out Of Memory),致使应用闪退
【本质】生命周期较长的对象持有生命周期较短的对象的引用
【内存泄漏与内存溢出】
如示例图所示,堆空间正常驻存对象数为8,空闲对象数为4,泄漏对象数为4,若待分配对象数为6,正常驻存对象全部被强引用时,将无法分配,导致发生 OOM。
内存泄漏不会必然造成内存溢出,但是大对象或多对象的内存泄漏会显著增加内存溢出的风险。
二 JVM 内存回收机制
1 JVM 内存模型
Java 程序加载到虚拟机后,对应的数据会存储到 JVM 运行时数据区对应的存储区域。其中方法区,堆为线程共享;虚拟机栈,本地方法栈,程序计数器为线程私有区域。
堆区为线程共享,承担对象的分配和回收,是内存泄漏发生的主要区域。
2 对象可达性分析
JVM 把对象中的引用看做一张图,从 GC Root 搜索引用链,根据引用链是否可达来决定是否回收对象,GC Root 对象本身是可达的不 GC 回收。
可用作 GC Root 的对象有:
- Java 虚拟机栈中引用对象,各个线程调用的参数、局部变量、临时变量等。
- 方法区中的类静态属性引用对象,比如引用类型的静态变量。
- 方法区中的常量引用对象。
- 本地方法栈的 JNI(native 方法)引用对象。
- Java 虚拟机内部的引用,基本数据类型对应的 Class 对象,一些常驻的异常对象。
- 被同步锁(synchronized)持有的对象。
3 堆内存回收
3.1 回收策略
Java堆区可以划分为新生代和老年代,新生代又可以进一步划分为Eden区、Survivor 0区、Survivor 1区。具体比例参数如下图所示
JVM 使用分代回收策略进行堆内存的管理和回收,整体流程如下:
1 对象默认在新生代 Eden 区分配,当 Eden 区满后,执行 GC,将存活的对象复制到 S0 区。
2 当下一次 Eden 区满时,执行对 Eden 和 S0 区 GC,将存活对象复制到 S1 区域。
3 Eden 满触发一定次数后,将依旧存活的对象复制到老年代中。
发生在新生代的 GC 叫做 minor GC,发生在老年代的 GC 叫做 major GC,若同时在新生代和老年代发生 GC 则成为 Full GC。
3.2 回收算法
3.2.1 标记-清除(Tracing Collector)
标记-清除(Tracing Collector)算法是最基础的收集算法。它使用了根集的概念,分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象。
优点: 不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
缺点: 标记和清除过程的效率都不高,需要使用一个空闲列表来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量;标记清除后会产生大量不连续的内存碎片。
3.2.2 标记整理算法 (Compacting Collector)
标记-整理(Compacting Collector)算法标记的过程与“标记-清除”算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于“标记-整理”算法的收集器的实现中,一般增加句柄和句柄表。
优点: 经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片问题。
缺点: GC 耗时更多,因为需要将所有对象都拷贝到新地方,且需更新它们的引用地址。
3.2.3 复制(Copying Collector)
复制(Copying Collector)算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。
优点: 标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。
缺点: 需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。
三 常见内存泄漏问题
1 Static 引用
【原因】若对象被 static 变量持有,引用耗费资源过多的实例(如 Context),则容易出现该成员变量的生命周期 > 引用实例生命周期的情况,当引用实例需结束生命周期销毁时,会因静态变量的持有而无法被回收,从而出现内存泄露。
【代码示例】
public class LeakSingleton {
private static LeakSingleton sInstance;
private Context mContext;
private LeakSingleton(Context context) {
mContext = context;
}
// LeakSingleton 传入 Context 对象引用,若 context 为 Activity,则 Activity 生命周期结束后,将继续被 LeakSignleton 持有
public synchronized LeakSingleton getInstance(Context context) {
return new LeakSingleton(context);
}
}
【典型场景】单例中使用 context 作为单例对象的成员变量,context 传入 Activity
2 非静态内部类
【原因】非静态内部类会持有外部类对象的引用,若非静态内部类对象生命周期超过外部类对象,则会发生外部类对象的内存泄漏。
【代码示例】
public class MainActivity extends AppCompatActivity {
// mHandler 引用匿名 Handler 对象
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
// 业务逻辑
}
};
private void sendDelayMessage() {
// 发送延时消息 10s 后执行,若 Activity 在此之前销毁,将被 mHandler 持有,发生泄漏
mHandler.sendEmptyMessageDelayed(1, 10 * 1000);
}
private void doWork() {
// 构建匿名 Thread 类对象执行耗时操作
new Thread(new Runnable() {
@Override
public void run() {
// 执行耗时操作,若在耗时操作完成前,Activity 销毁,将被 Thread 对象持有,发生泄漏
}
}).start();
}
}
【典型场景】Handler 发送延迟消息,开辟工作线程执行耗时操作。
3 资源对象使用后未关闭
【原因】 资源对象使用后未关闭,在 Activity/Fragemnt 销毁时没有关闭/注销这些资源,将导致无法回收。
【代码示例】
// 动态广播接收器未反注册
unregisterReceiver(mReceiver);
// 文件流操作未调用 close 关闭
InputStream inputStream = new FileInputStream(new File(inputFilePath));
OutputStream outputStream = new FileOutputStream(new File(outFilePath));
inputStream.close();
outputStream.close();
// 数据库游标未调用 close 方法
Cursor cursor = getContentResolver().query(url, projection, selection, null);
cursor.close();
// 无限重复执行动画
ObjectAnimator animator = ObjectAnimator.ofInt(view, property, startValue, endValue);
animator.setRepeatMode(ValueAnimator.INFINITE);
animator.start();
// 未调用 cancel
animator.cancel();
【典型场景】广播 BraodcastReceiver、文件流操作、数据库游标 Cursor、循环动画
四 常用内存泄漏分析工具
1 MemoryProfiler
MemoryProfiler 是 AndroidStudio 自带的内存分析工具,适用于在本地进行应用内存问题分析,其界面如下:
我们可以使用图中 2 所示功能执行 hprof dump,获取内存快照,根据 MemoryProfiler 提供的 leak 分析,确认程序中是否存在 Activity 或者 Fragment 泄漏。
详细的介绍和使用方法,请参考: https://developer.android.com/studio/profile/memory-profiler?hl=zh-cn
2 MAT(Memory Analyzer)
MAT 是 Eclipse 开发的强大堆内存分析工具,是分析内存使用和内存泄漏的利器。实际工作中,常用 Path to GC Roots 功能,分析对象的强引用链,从而定位内存泄漏问题。
上图展示了查看 Account 类强引用链的方法。
详细介绍和使用方法,请参考: http://wiki.eclipse.org/MemoryAnalyzer
3 LeakCanary
LeakCanary 是由 Square 公司开发的内存泄漏检测库,通过集成到 APK 中,方便应用开发者在线下开发调试阶段,发现内存泄漏问题,提供直观的内存泄漏原因 UI 展现。
LeakCanary 会将内存泄漏进行归类展示,如上图左侧图所示,检测出了 4 类泄漏(4 Distinck Leaks)。
如上图右侧所示,每个类别会呈现具体的泄漏引用链,在引用链的底部,显示了具体的泄漏原因。(TextView 成员变量 mContext 引用了销毁的 activity)。
详细介绍和使用方法,请参考: https://square.github.io/leakcanary/