Android常见内存泄漏

1.什么是内存泄露

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃(内存溢出OOM)等严重后果。
内存泄露的危害:

  • 用户对单次的内存泄漏并没有什么感知,但是当泄漏积累到内存都被消耗完,就会导致卡顿,甚至崩溃;
  • gc回收频繁 造成应用卡顿ANR:
  • 当内存不足的时候,gc会主动回收没用的内存.但是,内存回收也是需要时间的.
  • 内存回收和gc回收垃圾资源之间高频率交替的执行.就会产生内存抖动.
  • 很多数据就会污染内存堆,马上就会有许多GCs启动,由于这一额外的内存压力,也会产生突然增加的运算造成卡顿现象,
  • 任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行,所以垃圾回收运行的次数越少,对性能的影响就越少;

2.内存泄漏的原因

Android虚拟机有垃圾回收机制,会回收掉大部分的内存空间,但是有一些逻辑上已经不再使用的对象,被GC Root直接或间接引用,导致垃圾回收器不能回收它们。

2.1.虚拟机内存模型

Jvm(Java虚拟机)主要管理两种类型内存:堆和非堆。 堆是运行时数据区域,所有类实例和数组的内存均从此处分配。 非堆是JVM留给自己用的,包含方法区、虚拟机栈、本地方法栈、程序计数器。
Android常见内存泄漏_第1张图片
对于绝大多数应用来说,堆内存是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。堆内存由垃圾回收器的自动内存管理系统回收,出现内存泄漏也主要在这个区域

2.2.可回收对象的判定

可达性分析算法:从GC Roots(每种具体实现对GC Roots有不同的定义)作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之视为不可达。 即使循环引用了,只要没有被GC Roots引用了依然会被回收。 但是,这个GC Roots的定义就要考究了,Java语言定义了如下GC Roots对象:

虚拟机栈(帧栈中的本地变量表)中引用的对象。 方法区中静态属性引用的对象。 方法区中常量引用的对象。 本地方法栈中JNI引用的对象。
Android常见内存泄漏_第2张图片

3.内存泄漏检测工具

3.1.LeakCanary

通过集成 LeakCanary ,一旦检测到内存泄漏,LeakCanary 就会 dump Memory 信息,并通过另一个进程分析内存泄漏的信息并展示出来,随时发现和定位内存泄漏问题,极大地方便了Android应用程序的开发。

3.1.1.原理

LeakCanary 是通过在 Application 的 registerActivityLifecycleCallbacks 方法实现对 Activity 销毁监听的,在 Activity 在销毁时,将 Activity 包装到 WeakReference 中,被 WeakReference 包装过的 Activity 对象如果能够被回收,则说明引用可达,垃圾回收器就会将该 WeakReference 引用存放到 ReferenceQueue 中。相反就可能发生了内存泄漏。

3.1.2.接入
dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}

通过过滤 Logcat 中的 LeakCanary 标签来确认 LeakCanary 在启动时正在运行:

D/LeakCanary: LeakCanary is running and ready to detect memory leaks.

在 LeakCanary2.0 之前我们接入的时候需要在 Application.onCreate 方法中显式调用 LeakCanary.install(this); 开启 LeakCanary 的内存监控。
从 LeakCanary2.0 开始通过库里注册的 ContentProvier 自己开启 LeakCanary 的内存监控,无需用户手动再添加初始化代码。

3.1.3使用

安装测试应用后,手机上会出现如下两个图标:左侧的是测试应用的图标,右侧是自动安装的 LeakCanary 的图标。Android常见内存泄漏_第3张图片
打开可能存在内存泄漏页面,返回退出页面。等待大概10秒,LeakCanary 就会检测到,并进行分析。在通知栏可以看到进度。
Android常见内存泄漏_第4张图片
分析完成后,然后在通知栏会收到通知。
Android常见内存泄漏_第5张图片
点开通知,会打开 LeakCanary 的界面
Android常见内存泄漏_第6张图片
Android常见内存泄漏_第7张图片
另外,在 Logcat 中也可以看到 LeakCanary 的相关信息。LeakCanary 将应用中发现的泄漏分为两类:应用程序泄漏和库泄漏。

====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
......
====================================
0 LIBRARY LEAK
————————————————

3.2.Profiler

android studio自带的Profiler MEMORY工具,通过Capture heap dump抓取堆内存快照分析内存泄漏。
Android常见内存泄漏_第8张图片
如存在泄漏,会提示相应泄漏对象:
Android常见内存泄漏_第9张图片
根据泄漏对象references查看对象在哪里被引用,一般看是否被长生命周期对象(单例、静态对象、handler等)引用导致无法回收
Android常见内存泄漏_第10张图片

4.常见内存泄漏场景

4.1.单列、静态变量导致内存泄漏

单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄露。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。
常见的有:

  • Context:单例持有Activity、Service的Context。正确的做法是在单列、静态变量中如果需要用到Context可以用Application的Context替换
    反例:
public class UpdateManager {
    private static volatile UpdateManager mInstance;
    private Context mContext;

    private UpdateManager(Context context) {
        mContext = context;
    }

    public static UpdateManager getInstance(Context context) {
        if (mInstance == null) {
            synchronized (UpdateManager.class) {
                if (mInstance == null) {
                    mInstance = new UpdateManager(context);
                }
            }
        }
        return mInstance;
    }
}

public class StaticClass {
    // 定义1个静态变量
    private Static Context mContext;
     //...
     // 引用的是Activity或Service的context
     mContext = context; 
     // 当Activity或Service需销毁时,由于mContext = 静态 & 生命周期 = 应用程序的生命周期,故 Activity无法被回收,从而出现内存泄露
}

正例:

public class UpdateManager {
    //...省略代码
    private UpdateManager(Context context) {
        mContext = context.getApplicationContext();
    }
    //...省略代码
}
  • 监听回调:单例中只提供了添加监听的方法,无法取消监听。正确的做法是添加监听后在适当的位置取消监听
    反例:
public class UpdateManager {
    public void addUpdateListener(UpdateListener listener) {
        if (mUpdateListener.contains(listener)) {
            return;
        }
        mUpdateListener.add(listener);
    }
}

正例:

public class UpdateManager {
    public void addUpdateListener(UpdateListener listener) {
        if (mUpdateListener.contains(listener)) {
            return;
        }
        mUpdateListener.add(listener);
    }
    
    public void removeUpdateListener(UpdateListener listener) {
        mUpdateListener.remove(listener);
    }
}

4.2.非静态内部类、匿名类导致内存泄漏

非静态类和匿名类默认会持有外部类的引用,所以在长生命周期的类中引用了非静态内部类、匿名类就会导致内存泄漏。常见的解决方式是静态内部类+WeakReference

  • Handler:使用非静态内部类或匿名类定义Handler变量,由于使用的getMainLooper,那发送的Message回被添加在主线程的Looper中,其生命周期 = 应用的生命周期,Message又持有mHandler,mHandler是非静态内部类又持有外部类TestActivity,从而导致TestActivity无法回收
    反例:
public class TestActivity extends Activity {
    private Handler mHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 1) {
                showContent();
            }
        }
    };
    //使用mHandler.sendEmptyMessageDelayed(5000,1)发送消息,并未清理
}

正例一:在适当的位置情况已发送的Message

@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
}

正例二:使用静态内部类+WeakReference

private MyHandler mHandler;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mHandler = new MyHandler(this);
}

private static class MyHandler extends Handler {
    private final WeakReference<TestActivity> mWeakActivity;

    public MyHandler(TestActivity testActivity) {
        super();
        mWeakActivity = new WeakReference<>(testActivity);
    }

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        TestActivity testActivity = mWeakActivity.get();
        if (testActivity == null) {
            return;
        }
        if (msg.what == 1) {
            testActivity.showContent();
        }
    }
}
  • 多线程:AsyncTask、实现 Runnable 接口、继承 Thread 类,当工作线程正在处理任务时,如果外部类销毁, 由于工作线程实例持有外部类引用,将使得外部类无法被垃圾回收器(GC)回收,从而造成内存泄露。
    反例:
public class TestActivity extends Activity {
    private void execute() {
        new Thread() {
            @Override
            public void run() {
               doSomething();
            }
        }.start();
    }
    private void doSomething(){
      //Do time-consuming operations
    }
}

正例:同样可以使用静态内部类+WeakReference处理,但一般不直接new Thread进行异步操作,而是使用线程池,最好是用RxJava、kotlin协程,这样可以更好管理线程,并且可以在组件销毁时停止异步操作

public class TestActivity extends Activity {
    private static class MyThread extends Thread {
    private final WeakReference<TestActivity> mWeakActivity;
    public MyThread(TestActivity activity) {
        mWeakActivity = new WeakReference<>(activity);
    }
    @Override
    public void run() {
        super.run();
        if (mWeakActivity.get() == null) {
            return;
        }
        doSomething();
    }
    private void execute() {
        new MyThread(this).start();
    }
}

4.3.资源对象使用后未关闭

反例:未释放资源

public static void writeFile(File file, String content) {
    try {
        BufferedWriter bw = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8));
        bw.write(content);
        bw.flush();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

正例:在finally中close

public static void writeFile(File file, String content) {
    BufferedWriter bw = null;
    try {
        bw = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8));
        bw.write(content);
        bw.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (bw != null) {
            try {
                bw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4.4.系统服务

Android中有很多服务,比如PowerManager,AlarmManager,NotificationManager等,通常使用起来也很方便,就是使用Context.getSystemService方法来获得。但部分Manager会持有调用的Context,例如:PowerManager

public PowerManager(Context context, IPowerManager service, IThermalService thermalService,
        Handler handler) {
    mContext = context;
    mService = service;
    mThermalService = thermalService;
    mHandler = handler;
}

所以如果获取PowerManager的时候使用的是Activity或Service的Context,并且在单例或static类中保存了PowerManager,就会导致内存泄漏。泄漏分析过程可参考:https://cloud.tencent.com/developer/article/1328418
解决方案:

  1. 不使用静态持有Manager,就算Manager中持有Activity或Service的Context,只要生命周期与Activity或Service保存一致,也不会出现内存泄漏
  2. 使用Application Context获取系统服务。但注意和UI相关的服务在Activity或ContextThemeWrapper中会做了优化,如果全部都使用Application Context将会得不偿失,所有需要试情况而定
//ContextThemeWrapper也优先处理了LayoutManager服务
@Override
public Object getSystemService(String name) {
    if (LAYOUT_INFLATER_SERVICE.equals(name)) {
        if (mInflater == null) {
            mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
        }
        return mInflater;
    }
    return getBaseContext().getSystemService(name);
}

a. 如果服务和UI相关,则用Activity
b. 如果是类似ALARM_SERVICE,CONNECTIVITY_SERVICE建议有限选用Application Context
c. 如果出现出现了内存泄漏,排除问题,可以考虑使用Application Context

4.5.广播未注销

无论是系统广播还是本地广播,注册与反注册需成对出现

你可能感兴趣的:(Android,android,jvm,java)