Android 性能优化:手把手带你全面了解内存泄露
public class Sample {
// 该类的实例对象的成员变量 s1、mSample1 & 指向对象存放在堆内存中 int s1 = 0;Sample mSample1 = new Sample();
// 方法中的局部变量 s2、mSample2 存放在 栈内存// 变量 mSample2 所指向的对象实例存放在 堆内存// 该实例的成员变量 s1、mSample1 也存放在栈中 public void method() {
int s2 = 0;Sample mSample2 = new Sample();}}// 变量 mSample3 所指向的对象实例存放在堆内存中// 该实例的成员变量 s1、mSample1 也存放在堆内存中 Sample mSample3 = new Sample();
4.2 内存释放策略
此处主要讲解对象分配(即堆式分配)的内存释放策略 = Java
垃圾回收器(GC
)
由于静态分配不需释放、栈式分配仅 通过帧栈自动出、入栈,较简单,故不详细描述
Java
垃圾回收器(GC
)的内存释放 = 垃圾回收算法,主要包括:
具体介绍如下
5. 常见的内存泄露原因 & 解决方案
5.1 集合类
内存泄露原因 集合类 添加元素后,仍引用着 集合元素对象,导致该集合元素对象不可被回收,从而 导致内存泄漏
实例演示
// 通过 循环申请 Object 对象 & 将申请的对象逐个放入到集合 ListList<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {Object o = new Object();objectList.add(o);o = null;}// 虽释放了集合元素引用的本身:o=null)// 但集合 List 仍然引用该对象,故垃圾回收器 GC 依然不可回收该对象
解决方案 集合类 添加集合元素对象 后,在使用后必须从集合中删除
由于 1
个集合中有许多元素,故最简单的方法 = 清空集合对象 & 设置为null
// 释放 objectListobjectList.clear();objectList=null;
5.2 Static 关键字修饰的成员变量
储备知识 被
Static
关键字修饰的成员变量的生命周期 = 应用程序的生命周期泄露原因 若使被
Static
关键字修饰的成员变量 引用耗费资源过多的实例(如Context
),则容易出现该成员变量的生命周期 > 引用实例生命周期的情况,当引用实例需结束生命周期销毁时,会因静态变量的持有而无法被回收,从而出现内存泄露实例讲解
public class ClassName {// 定义 1 个静态变量 private static Context mContext;//...// 引用的是 Activity 的 contextmContext = context;
// 当 Activity 需销毁时,由于 mContext = 静态 & 生命周期 = 应用程序的生命周期,故 Activity 无法被回收,从而出现内存泄露
}
解决方案
尽量避免
Static
成员变量引用资源耗费过多的实例(如Context
)
若需引用
Context
,则尽量使用Applicaiton
的Context
使用 弱引用
(WeakReference)
代替 强引用 持有实例
注:静态成员变量有个非常典型的例子 = 单例模式
储备知识 单例模式 由于其静态特性,其生命周期的长度 = 应用程序的生命周期
泄露原因 若 1 个对象已不需再使用 而单例对象还持有该对象的引用,那么该对象将不能被正常回收 从而 导致内存泄漏
实例演示
// 创建单例时,需传入一个 Context// 若传入的是 Activity 的 Context,此时单例 则持有该 Activity 的引用// 由于单例一直持有该 Activity 的引用(直到整个应用生命周期结束),即使该 Activity 退出,该 Activity 的内存也不会被回收// 特别是一些庞大的 Activity,此处非常容易导致 OOM
public class SingleInstanceClass {
private static SingleInstanceClass instance;
private Context mContext;
private SingleInstanceClass(Context context) {
this.mContext = context; // 传递的是 Activity 的 context}
public SingleInstanceClass getInstance(Context context) {
if (instance == null) {instance = new SingleInstanceClass(context);}
return instance;}}
解决方案 单例模式引用的对象的生命周期 = 应用的生命周期
如上述实例,应传递
Application
的Context
,因Application
的生命周期 = 整个应用的生命周期
public class SingleInstanceClass {
private static SingleInstanceClass instance;
private Context mContext;
private SingleInstanceClass(Context context) {
this.mContext = context.getApplicationContext(); // 传递的是 Application 的 context}
public SingleInstanceClass getInstance(Context context) {
if (instance == null) {instance = new SingleInstanceClass(context);}
return instance;}}
5.3 非静态内部类 / 匿名类
储备知识 非静态内部类 / 匿名类 默认持有 外部类的引用;而静态内部类则不会
常见情况 3 种,分别是:非静态内部类的实例 = 静态、多线程、消息传递机制(
Handler
)
5.3.1 非静态内部类的实例 = 静态
泄露原因 若 非静态内部类所创建的实例 = 静态(其生命周期 = 应用的生命周期),会因 非静态内部类默认持有外部类的引用 而导致外部类无法释放,最终 造成内存泄露
即 外部类中 持有 非静态内部类的静态对象
实例演示
// 背景:a. 在启动频繁的 Activity 中,为了避免重复创建相同的数据资源,会在 Activity 内部创建一个非静态内部类的单例 b. 每次启动 Activity 时都会使用该单例的数据
public class TestActivity extends AppCompatActivity {
// 非静态内部类的实例的引用// 注:设置为静态
public static InnerClass innerClass = null;
@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 保证非静态内部类的实例只有 1 个 if (innerClass == null)innerClass = new InnerClass();}
// 非静态内部类的定义
private class InnerClass {
//...}}
// 造成内存泄露的原因:// a. 当 TestActivity 销毁时,因非静态内部类单例的引用(innerClass)的生命周期 = 应用 App 的生命周期、持有外部类 TestActivity 的引用// b. 故 TestActivity 无法被 GC 回收,从而导致内存泄漏
解决方案
将非静态内部类设置为:静态内部类(静态内部类默认不持有外部类的引用)
该内部类抽取出来封装成一个单例
尽量 避免 非静态内部类所创建的实例 = 静态
若需使用
Context
,建议使用Application
的Context
5.3.2 多线程:AsyncTask、实现 Runnable 接口、继承 Thread 类
储备知识 多线程的使用方法 = 非静态内部类 / 匿名类;即 线程类 属于 非静态内部类 / 匿名类
泄露原因 当 工作线程正在处理任务 & 外部类需销毁时, 由于 工作线程实例 持有外部类引用,将使得外部类无法被垃圾回收器(GC)回收,从而造成 内存泄露
多线程主要使用的是:
AsyncTask
、实现Runnable
接口 & 继承Thread
类前 3 者内存泄露的原理相同,此处主要以继承
Thread
类 为例说明
实例演示
/**
方式 1:新建 Thread 子类(内部类)*/
public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);
// 通过创建的内部类 实现多线程 new MyThread().start();
}// 自定义的 Thread 子类 private class MyThread extends Thread{@Overridepublic void run() {try {Thread.sleep(5000);Log.d(TAG, "执行了多线程");} catch (InterruptedException e) {e.printStackTrace();}}}}
/**
方式 2:匿名 Thread 内部类*/public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";
@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);
// 通过匿名内部类 实现多线程 new Thread() {@Overridepublic void run() {try {Thread.sleep(5000);Log.d(TAG, "执行了多线程");} catch (InterruptedException e) {e.printStackTrace();}
}}.start();}}
/**
分析:内存泄露原因*/// 工作线程 Thread 类属于非静态内部类 / 匿名内部类,运行时默认持有外部类的引用// 当工作线程运行时,若外部类 MainActivity 需销毁// 由于此时工作线程类实例持有外部类的引用,将使得外部类无法被垃圾回收器(GC)回收,从而造成 内存泄露
解决方案 从上面可看出,造成内存泄露的原因有 2 个关键条件:
存在 ”工作线程实例 持有外部类引用“ 的引用关系
工作线程实例的生命周期 > 外部类的生命周期,即工作线程仍在运行 而 外部类需销毁
解决方案的思路 = 使得上述任 1 条件不成立 即可。
// 共有 2 个解决方案:静态内部类 & 当外部类结束生命周期时,强制结束线程// 具体描述如下
/**
解决方式 1:静态内部类
原理:静态内部类 不默认持有外部类的引用,从而使得 “工作线程实例 持有 外部类引用” 的引用关系 不复存在
具体实现:将 Thread 的子类设置成 静态内部类*/
public class MainActivity extends AppCompatActivity {
public static final String TAG = "carson:";@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);
// 通过创建的内部类 实现多线程 new MyThread().start();
}// 分析 1:自定义 Thread 子类// 设置为:静态内部类 private static class MyThread extends Thread{@Overridepublic void run() {try {Thread.sleep(5000);Log.d(TAG, "执行了多线程");} catch (InterruptedException e) {e.printStackTrace();}}}}
/**
解决方案 2:当外部类结束生命周期时,强制结束线程
原理:使得 工作线程实例的生命周期 与 外部类的生命周期 同步
具体实现:当 外部类(此处以 Activity 为例) 结束生命周期时(此时系统会调用 onDestroy()),强制结束线程(调用 stop())*/@Overrideprotected void onDestroy() {super.onDestroy();Thread.stop();// 外部类 Activity 生命周期结束时,强制结束线程}
评论