Earth Guardian

You are not LATE!You are not EARLY!

0%

Android 内存泄露

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏会使应用占用的内存,随着时间不断的增加而造成应用 OOM(Out Of Memory) 错误,使应用崩溃。

基础概念

内存分配及垃圾回收 GC

内存分配及垃圾回收的基础知识,参考:JVM 内存分配及垃圾回收-内存分配

小结:

  • GC 回收的对象必须是不可达的,或者当前没有任何引用的对象
  • 当对象在使用完成后(对我们而言已经是垃圾对象了),如果没有释放该对象的引用,会导致 GC 不能回收该对象而继续占用内存
  • 垃圾对象持续占用内存,导致内存空间的浪费,就发生内存泄露了
  • 大量的内存泄露导致有效内存减少,当再次合理申请不到足够内存时,则会出现内存溢出

内存泄露和内存溢出

  • 内存泄露

垃圾对象依旧占据堆内存,没有得到正确的释放。水池的水在使用后需要将脏水排空,但是排水管修的太高,有一部分脏水占用了水池的空间。

  • 内存溢出

内存占用达到最大值,当再需要时已经无法分配,这就是内存溢出。水池已经满了,这时再放水进来就会溢出。

内存的溢出是内存分配达到了最大值,而内存泄漏是无用内存充斥了内存堆;内存泄漏会占用内存堆导致可用内存太少,很容易出现内存溢出现象。

常见内存泄露场景

静态变量与内存泄露

Java 中静态变量在类加载时初始化,并存储在方法区是类变量,在类卸载的时候销毁并释放清空。下面简单介绍下类加载和卸载,参考java 静态变量及类的生命周期

类加载

遇到 new, getstatic, putstatic, invokestatic 这 4 条字节码指令时、反射调用时、子类初始化时、虚拟机启动 main 主类时等等,会触发类加载并初始化。

静态变量

虚拟机在加载类的过程中为静态变量分配内存,static 变量在内存中只有一个,存放在方法区,属于类变量,被所有实例所共享。

类卸载

方法区会回收两部分内容:废弃常量和无用的类。类被回收时才会卸载,但是方法区回收类时条件比较严格,只有同时满足以下三个条件,才会回收:

  • 该类所有的实例都已经被回收(GC),也就是虚拟机中不存在该 Class 的任何实例
  • 加载该类的 ClassLoader 已经被回收(GC
  • 该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问改类的方法
    静态变量在类没有被销毁,也没有置 null 的情况下,是不会被回收的(GC)。

安全可靠性

Android 在资源不足的时候会杀掉一些进程,在资源足够时重启被杀掉的进程,这时可能会存在某些应用内存不足被杀后重启了。这会导致 Android 中的静态变量并不可靠,上次保存的静态数据可能没了,所以针对静态变量,必须保存一份到本地文件。另外,静态变量在进程退出时才会被销毁,所以在多账号的应用中,退出账号时上次保存的数据还在,需要手动重置那些与账户有关的静态数据,以免影响到另一个账户。

结论

静态变量只有在类卸载(条件很苛刻),虚拟机关闭,进程被杀等情况下,才会被回收。通常我们可以简单理解,Android 的静态变量和整个应用的生命周期相同,所以静态变量引用了当前类中的变量,会引起当前类对象无法被及时销毁,导致内存泄露。

静态变量要尽量少用,在用完后必须置空,养成好习惯。

非静态内部类与内存泄露

Java 中,非静态内部类都会持有外部类的引用,参考Java 内部类
通过反编译非静态内部类对应的 class 文件,可以看出内部类在构造方法中,传入了外部类的引用 this$0,内部类在访问外部类的成员或方法时,都需要传递该参数 this.this$0,只有静态内部类例外。这也可看出为什么静态内部类在实例化时不需要外部类实例化,而其他内部类在实例化时必须先实例化外部类了。
内部类虽然和外部类写在同一个文件中,但是编译完成后会生成各自的 class 文件,编译过程中:

  • 编译器自动为非静态内部类添加一个成员变量,这个成员变量的类型和外部类的类型相同,这个成员变量就是指向外部类对象的引用
  • 编译器自动为非静态内部类的构造方法添加一个参数,参数的类型是外部类的类型,这个参数为内部类中添加的成员变量赋值
  • 在调用非静态内部类的构造函数初始化内部类对象时,会默认传入外部类的引用

换句话说:静态内部类不持有外部类对象的引用,而其他内部类都会持有。在 Android 中经常会使用成员内部类或者匿名内部类,特别是这些类开后台线程或者做耗时操作时,会引起外部类在退出时(仍然被非静态内部类持有引用)无法被释放,导致内存泄露。当然这种后台因为耗时任务引起的内存泄露,在耗时任务执行完后,会释放外部类的引用,从而再下次 GC 时,外部类可以被正常回收。如果后台任务为无限循环,则外部类会被持续持有

尽量使用静态内部类,避免持有外部类的引用。

强弱软引用与内存泄露

为了规避内存泄露,通常使用软引用或弱引用。参考:JVM 内存分配及垃圾回收-对象引用的四种分类

注册和取消注册

监听系统服务,通常要将自己 Context 注册到系统中,这会导致服务持有了 Context 的引用,如果在 Activity 销毁的时没有注销这些监听器,会导致内存泄漏。所以注册和取消注册一定是成对出现的:

1
2
registerListener();
unregisterListener();

Bitmap 使用不当造成内存泄露和溢出

Bitmap 非常容易导致内存溢出,通常 1200 万像素的手机拍下来的照片为 4048x3036 像素,如果默认配置为 ARGB_8888 (4 个字节存储),打开一张这样的图片大概需要 4048*3036*4/1024=48M 大小的内存,很容易导致应用内存溢出。所以 Bitmap 使用时需要非常小心并及时回收。参考 Android 官方文档:Handling BitmapsLoading Large Bitmaps EfficientlyCaching BitmapsManaging Bitmap Memory

其他可能引起的内存泄露

  • 资源性对象未关闭
    Cursor, File, Socket 等的使用,最后需要 close 并置空。
  • 屏幕旋转导致的 Activity 重建
    反复旋转设备经常会导致应用泄漏 Activity, Context, View 对象,因为系统会重新创建 Activity,而如果在其他地方保持对这些对象之一的引用,系统将无法对其进行垃圾回收。

常见内存泄露示例

内存泄露的根本原因:长生命周期对象引用了短生命周期对象导致。当短生命周期对象结束后,而长生命周期仍然持有这个引用,导致短生命周期对象无法被释放

静态变量

根据上面分析,静态变量很容易引起内存泄露,应该尽量少用,用完后退出必须置空。如下为静态 Activity 变量和静态 View 引起的内存泄露示例。

错误示例

1
2
3
4
5
6
7
8
9
10
11
private static Activity sLeakActivity;
private void staticActivityLeak(){
Log.d(TAG, "staticActivityLeak: ");
sLeakActivity = this;
}

private static View sLeakView;
private void staticViewLeak(View view){
Log.d(TAG, "staticViewLeak: ");
sLeakView = view;
}

示例很简单,定义了两个静态变量分别保存当前 Activity 变量和某个 View 变量,而静态变量生命周期基本和应用生命周期相同,所以当前 Activity 退出时,因为静态变量持有它的引用,导致 Activity 实例无法被回收出现内存泄露。

正确示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 使用非静态变量保存
private Activity mSafeActivity;
private void safeActivity(){
Log.d(TAG, "safeActivity: ");
mSafeActivity = this;
}

@Override
protected void onDestroy() {
super.onDestroy();
// 2. 静态变量置空
if (mSafeView != null){
mSafeView = null;
}
}

解决方法也很简单,不使用静态变量,或者在 Activity.onDestroy 时,将静态变量置空,断开引用链关系。

单例模式

Android 中,单例的静态特性使得单例的生命周期和应用的生命周期一样长,而单例中引用了 Activity 对象。当该 Activity 退出时,因为单例还持有这个对象,导致该 Activity 无法被回收,导致内存泄露。

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MemLeakSample extends AppCompatActivity {
// 1. Singleton
private static class SingletonLeak{
private static SingletonLeak sInstance;
private Context mContext;
private SingletonLeak(Context context){
mContext = context;
}

public static SingletonLeak getInstance(Context context){
if (sInstance == null){
// 构造方法,单例持有了 Activity 的引用
// 导致 Activity 退出时,无法被回收
sInstance = new SingletonLeak(context);
}
return sInstance;
}
}

// 2. 获取单例,传入了 Activity
private void staticSingletonLeak(){
SingletonLeak.getInstance(this);
}
}

在这个示例中,MemLeakSample 中调用了单例 SingletonLeak,并将自身传递给单例,紧接着马上关闭退出。正常情况下 MemLeakSample 退出后,应当释放并回收,但是因为静态单例持有了 Context 的引用,导致其无法被回收,引起内存泄露。

正确示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. Singleton
private static class SafeSingleton{
private static SafeSingleton sInstance;
private Context mContext;
private SafeSingleton(Context context){
mContext = context;
}

public static SafeSingleton getInstance(Context context){
if (sInstance == null){
// 构造方法,单例持有的是应用的 Context
// 和当前 Activity 无关,不影响 Activity 的回收
sInstance = new SafeSingleton(context.getApplicationContext());
}
return sInstance;
}
}

// 2. 获取单例,传入 Activity
private void safeStaticSingleton(){
SafeSingleton.getInstance(this);
}

Context 赋值为整个应用的上下文 this.context = context.getApplicationContext();,这样单例 Context 就和应用的生命周期相同了,和具体的 Activity 无关,所以 MemLeakSample 退出后,直接释放并回收。

匿名内部类 - Thread/Runnable

后台耗时任务的匿名内部类,可能出现内存泄露。

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
private void anonymousThreadLeak(){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(30000);
Log.d(TAG, "run: anonymousThreadLeak, runnable, exit.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}

匿名 Runnable 在后台执行耗时任务,当前 Activity 退出,因为 Runnable 持有 Activity 的引用,导致出现内存泄露。但是当 Runnable 执行完后,会释放引用,下次 GC 时,Activity 能被正常回收。当然在编码时,更希望不要出现泄露的可能性。

正确示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用静态内部类,避免持有外部类引用
private static class MyRunnable implements Runnable{
@Override
public void run() {
try {
Thread.sleep(30000);
Log.d(TAG, "run: MyRunnable, exit.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

private void safeAnonymousThread(){
new Thread(new MyRunnable()).start();
}

使用静态内部类,避免持有外部类引用,从根本上避免内存泄露。和 Thread/Runnable 类似的还有 AsyncTask,如果异步任务没有执行完而 Activity 退出,就会导致内存泄露。

匿名内部类 - Handler

所有 Message 都持有 Handler 的引用,而匿名内部类 Handler 会持有外部 Activity,形成引用链。

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
private Handler mLeakHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Log.d(TAG, "mLeakHandler.handleMessage: msg.what = " + msg.what);
mTvNonStaticInnerClass.setTextColor(Color.RED);
while (true){}
}
};

private void anonymousHandlerLeak(){
mLeakHandler.sendEmptyMessageDelayed(MSG_LEAK, 30000);
}

匿名 Handler 在处理延时消息这段时间时,如果 Activity 退出,而匿名 Handler 持有它的引用,导致 Activity 无法被正常释放,引起内存泄露。

正确示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static class SafeHandler extends Handler{

private WeakReference<MemLeakSample> mActivity;

public SafeHandler(MemLeakSample activity){
mActivity = new WeakReference<>(activity);
}

@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Log.d(TAG, "mSafeHandler.handleMessage: msg.what = " + msg.what);
//mActivity.get().mTvNonStaticInnerClass.setTextColor(Color.BLUE);
}
};
private SafeHandler mSafeHandler;

private void safeAnonymousHandler(){
mSafeHandler = new SafeHandler(this);
mSafeHandler.sendEmptyMessageDelayed(MSG_SAFE, 30000);
}

使用静态内部类继承 Handler,避免持有外部 Activity 的引用。但是 Handler 通常来更新 UI,如果使用静态内部类,则无法正常访问 ActivityUI 控件了,这里采用弱引用的方式,保存 Activity 的实例来更新 UI,确保在 GC 时,Activity 能被正常回收。

注册系统服务监听

通过 Context.getSystemService(int name) 获取系统服务,这些服务工作在各自的进程中,如果需要使用这些服务,可以注册监听器,这会导致服务持有了 Context 的引用,如果在 Activity 销毁的时没有注销这些监听器,会导致内存泄漏。

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private SensorManager mSensorManager;

private MySensorEventListener mySensorEventListener;
private static class MySensorEventListener implements SensorEventListener{
@Override
public void onSensorChanged(SensorEvent event) {

}

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {

}
}

private void registerSensor(){
mySensorEventListener = new MySensorEventListener();
mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
Sensor sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ALL);
mSensorManager.registerListener(mySensorEventListener,
sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

private void registerLeak(){
isRegisterSafe = false;
registerSensor();
}

private boolean isRegisterSafe;

正确示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void safeRegister(){
isRegisterSafe = true;
registerSensor();
}

@Override
protected void onDestroy() {
super.onDestroy();

if (isRegisterSafe && mSensorManager != null
&& mySensorEventListener != null){
mSensorManager.unregisterListener(mySensorEventListener);
mSensorManager = null;
mySensorEventListener = null;
}
}

注册后,需要在 Activity.onDestroy 中取消注册,避免系统服务持有当前 Activity 的引用,从而规避内存泄露。

内存泄露检测工具

Android Profiler['proʊfaɪlə(r)]Android Studio 3.0 推出的一个监控工具,分为三大模块:CPU、内存 、网络。利用 Memory Profiler 来监控并分析当前应用内存的使用情况,官网地址:Android Profiler Memory Profiler
Memory Profiler 可以识别导致应用卡顿、冻结甚至崩溃的内存泄漏和流失。能够显示应用内存使用量的实时图表,捕获堆转储、强制执行垃圾回收以及跟踪内存分配等功能。

简介

打开方式:点击 View > Tool Windows > Android Profiler,或者点击工具栏中的 Android Profiler 图标打开 Android Profiler。然后点击 MEMORY 时间线中的任意位置可打开 Memory Profiler

0059-memory-profiler-callouts_2x.png

图片中 1-7 按钮分别表示:

  1. 用于强制执行垃圾回收的按钮(GC)。
  2. 用于捕获堆转储的按钮。
  3. 用于记录内存分配情况的按钮。此按钮仅在连接至运行 Android 7.1 或更低版本的设备时才会显示。
  4. 用于放大/缩小时间线的按钮。
  5. 用于跳转至实时内存数据的按钮。
  6. Event 时间线,其显示 Activity 状态、用户输入 Event 和屏幕旋转 Event
  7. 内存使用量时间线,其包含以下内容:
  • 一个显示每个内存类别使用多少内存的堆叠图表,如左侧的 y 轴以及顶部的彩色键所示。
  • 虚线表示分配的对象数,如右侧的 y 轴所示。
  • 用于表示每个垃圾回收 Event 的图标。

如果遇到:Advanced profiling is unabailable for the selected process 这个问题,是因为在 Android 7.1 或更低版本的设备需要开启 Profiler 配置,Android 8.0 及以上不会存在。
解决方案:Run -- Edit Configurations... 打开应用配置界面,选择应用并在右边的 tab 中勾选 Enable advanced profiling

0059-enable-advanced-profiling.png

查看内存泄露

点击 Memory Profiler 的堆转储按钮,捕获应用中象使用内存的当前状态。特别是在长时间的用户会话后,堆转储会显示那些不应再位于内存中却仍在内存中的对象,从而帮助识别内存泄漏。堆转储中可以看到:

  • 应用已分配哪些类型的对象,以及每个类型分配多少
  • 每个对象正在使用多少内存
  • 在代码中的何处仍在引用每个对象

Instance View 中,每个实例都包含以下信息:

  • Depth
    从任意 GC 根到所选实例的最短 hop 数,如果不为 0 ,则表示无法回收(内存泄露可疑点)。
  • Shallow Size:此实例的大小
  • Retained Size:此实例支配的内存大小

0059-memeory-profiler-mem-leak.png

Memory Profiler 中只能看出内存泄露产生了,主要是按包查看指定应用的实例引用数(Depth)是否为 0 。如果不为 0 ,基本可以确定存在内存泄露了,需要查看代码慢慢检查。如果为 0,下次 GC 时会回收这些对象。

分析 hprof 文件

HPROF 文件是一种二进制堆转储格式文件,包含了内存相关信息,可以直接使用 AS 打开这类文件。

0059-hprof-analyzer-task.png

从图中可以看出,打开右上角的 Analyzer Tasks 页,点击开始按钮后,自动分析 hprof 文件中 Activity/String的内存泄露,通常我们用来看 Activity 的内存泄露。点击泄露的 Activity 后,在左下角能看到调用关系,可以很明确的得出具体是哪个变量引用没有释放。
参考网页:HPROF文件查看和分析工具

其他检查工具

参考文档