本篇博客主要是记录一下Android内存泄露相关的问题。
网上有很多介绍内存泄露的文章,自己摘取了一些比较有价值的内容,
同时增加了一些自己的理解。
在分析内存泄露前,我们必须先对内存泄露的定义有所了解。
简单来讲,Android对内存泄露的定义,与Java中的定义基本一致,即:
正常情况下,当一个对象已经不需要再被使用时,它占用的内存就能够被系统回收。
如果一个本该被回收的无用对象,由于被其它有效对象引用,使得对应的内存不能被系统回收,就称之为内存泄漏。
我们知道Android系统为每个应用分配的内存有限,当一个应用中产生的内存泄漏比较多时,
就难免会导致应用所需要的内存超过系统分配的极限,于是就造成应用出现了OOM错误。
因此,每个开发人员有必要对内存泄露的原理、出问题的场景及分析工具有一定的了解。
一、Java内存分配策略
Java 程序运行时的内存分配策略有三种,分别是静态分配、栈式分配和堆式分配。
对应的,三种分配策略使用的内存空间分别是静态存储区、栈区和堆区。
其中:
1、静态存储区:主要存放静态数据和常量。
这部分内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
2、栈区 :当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结束时,自动释放其持有的内存。
由于栈内存分配相关的运算,内置于处理器的指令集中,因此执行效率很高,不过其分配的内存容量有限。
3、堆区 : 主要用于存储动态分配的对象。
这部分内存在不使用时,将会由 Java 的垃圾回收器来负责回收。
举例来说:
public class Example {
//静态存储区
static int e1 = 0;
//堆区
int e2 = 1;
Example e3 = new Example();
void method() {
//栈区
int e4 = 2;
Example e5 = new Example();
}
}
如上面代码所示,e1作为静态变量,是与Example这个类关联的,将被分配到静态存储区。
e2和e3均是一个具体对象的成员变量,由于对象必须被动态创建出来,因此e2和e3均将被分配到堆区。
即类中定义的非静态成员变量全部存储于堆中,包括基本数据类型、引用和引用指向的对象实体。
对于一个具体的方法来说,如代码中的method,当方法执行时,其内部的临时变量e4、e5均分配在栈区;
当方法执行完毕后,e4和e5的内存均会被自动释放。
这里需要注意的是,e5分配在栈区,但其指向的对象是分配在堆区的。
由此可以看出,局部变量的基本数据类型和引用存储于栈区,引用指向的对象实体存储于堆区。
二、Java内存释放策略
1、原理
Java的内存释放策略是由GC(Garbage Collection)机制决定和执行的,主要针对的是堆区内存。
GC为了能够准确及时地释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。
关于GC释放内存的原理,参考了一些资料,个人觉得一种比较好的理解方式是:
将堆区对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。
将每个线程对象作为一个图的起始顶点,从起始顶点可达的对象都是有效对象,不会被GC回收;
如果某个对象从起始顶点出发不可达,那么这个对象就可以被认为是无效的,可以被 GC 回收。
对于程序运行的每一个时刻,都可以用一个有向图表示JVM的内存分配情况。
举例来说,对于下面的代码:
public class Solution {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Test();
o2 = o1;
//运行到此处,看一下对应的GC有向图
.................
}
}
public class Test {
private Object o3;
Test() {
o3 = new Object();
}
}
程序运行到注释行时,对应的GC有向图大致如下:
main函数所在进程为有向图的根节点。
当函数运行到注释行时,没有从根节点到Test对象和Obj 3的路径,
因此Test和Obj 3对象均可以被GC回收。
注意对象能否被回收的依据是,是否存在从根节点到该对象的路径,
因此虽然Obj 3被引用了,但依然会被回收。
由上述例子可以看出,Java的GC机制使用有向图的方式进行内存管理,可以消除引用循环的问题。
例如有三个对象相互引用,只要它们对根进程而言是不可达的,那么GC也可以对它们进行回收。
GC的这种内存管理方式的优点是精度很高,但是效率较低。
另外一种常用的内存管理技术是使用计数器,它与有向图相比精度较低(很难处理循环引用的问题),但执行效率较高。
最后需要提一点的是,对于程序员来说,GC对应的操作基本是透明的。
虽然可以主动调用几个GC相关的函数,例如System.gc()等,但是根据Java语言规范定义,
这些函数并不保证GC线程一定会进行实际的工作。
这是因为不同的JVM可能使用不同的算法管理GC,例如:
有的JVM检测到内存使用量到达门限时,才调度GC线程进行工作;
有的JVM是定时调度GC线程进行工作等。
2、Java内存泄露的例子
结合Java的GC机制,我们知道了,对于Java中分配在堆内存的对象而言:
如果某个对象是可达的,即在有向图中,存在从根节点到这些对象的路径;
同时这个对象是无用的,即程序以后不会再使用这个对象;
那么该对象就被判定为Java中的内存泄漏。
关于Java内存泄露的例子,可以参考下面的代码:
public class Example {
private static ArrayList<Object> testArray = new ArrayList<>();
public void test() {
Object o = new Object();
testArray.add(o);
//本意是想主动释放掉内存,但Obj被testArray持有引用
//因此,对应的堆内存无法释放掉
o = null;
}
}
类似上面的例子,如果集合类是全局性的变量,同时没有相应的删除机制,则很可能导致集合所占用的内存只增不减。
三、Android中的内存泄露举例
接下来我们看看Android中一些内存泄露的例子。
1、静态单例对象引入的泄露
静态对象生命周期的长度与整个应用一致,
如果静态对象持有了一个生命周期较短的对象,
例如Activity等,那么就会导致内存泄露。
这种错误经常出现在使用单例对象的场景中,例如:
public class SingleInstance {
private final static Object LOCK = new Object();
//单例模式需要静态对象
private static SingleInstance singleInstance;
//静态对象持有Context就可能导致内存泄露
private Context mContext;
public static SingleInstance getInstance(Context context) {
synchronized (LOCK) {
if (singleInstance == null) {
return new SingleInstance(context);
}
return singleInstance;
}
}
private SingleInstance(Context context) {
mContext = context;
}
}
如上面的代码所示:
如果获取单例模式时传的是Application的Context,
由于Application的生命周期就是整个应用的生命周期,
即Context与静态对象的生命周期一致,没有任何问题;
如果传入的是 Activity 等的 Context,那么当这个 Context 所对应的 Activity 退出时,
由于该 Context 的引用被静态单例对象所持有,而单例对象将持续到应用结束,
于是即使当前 Activity 退出,它的内存也不会被回收,就造成了内存泄漏。
由此可以看出,在Android中尽量不要让静态对象持有Context。
如果静态对象一定要持有Context,就让它持有Application Context,
即上面代码需要更改为:
public class SingleInstance {
private final static Object LOCK = new Object();
//Android Studio的静态代码检查,会提示不要将Context类置于静态引用中,可能会导致内存泄露
private static SingleInstance singleInstance;
private Context mContext;
public static SingleInstance getInstance(Context context) {
synchronized (LOCK) {
if (singleInstance == null) {
return new SingleInstance(context);
}
return singleInstance;
}
}
private SingleInstance(Context context) {
//若实在需要用,就获取Application Context
mContext = context.getApplicationContext();
}
}
不过Application Context也不是万能的,有些场景下Application Context是无法使用的,例如创建一个Dialog。
关于Application、Activity和Service三者的Context应用场景,自己也没有总结过,
就截一下参考资料中的图吧,有机会再深入研究一下:
图中,NO1表示 Application 和 Service 可以启动一个 Activity,不过需要创建一个新的 task 任务队列。
2、非静态内部类引入的泄露
如下代码所示,在MainActivity的onCreate函数中,创建了一个非静态内部类对象,
该对象被Activity中的一个静态对像引用。
public class MainActivity extends AppCompatActivity {
private static Resource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (mResource == null) {
mResource = new Resource();
}
}
private class Resource {
//........
}
}
在上述代码对应的场景中,由于非静态内部类默认会持有外部类的引用,
而内部类的一个实例又被一个静态对象持有,于是最终导致外部类Activity被一个静态对象持有。、
正如前文提及的,由于静态对象一直存在,于是Activity退出时,对应的内存也没法被GC机制回收。
这种问题的解决方案就是,将非静态内部类变为静态内部类,或抽取成一个单独的类。
3、自定义Handler引入的泄露
非静态内部类引入内存泄漏的场景中,比较典型的就是自定义Handler引入的内存泄露:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//这种方式获取Handler,实际上绑定的是sThreadLocal中的Looper
Handler handler = new MayLeakHandler();
//延迟处理一个消息
//这里匿名内部类其实也会持有外部类的引用
handler.postDelayed(new Runnable() {
@Override
public void run() {
//.......
}
}, 6000);
//Activity界面关闭,但其内存还是将被Handler对应的静态线程持有
finish();
}
//定义一个非静态内部类
private class MayLeakHandler extends Handler {
@Override
public void handleMessage(Message msg) {
//.............
}
}
}
上面代码产生内存泄露的原因是:
MayLeakHandler是一个非静态内部类,持有对Activity的引用。
当Activity退出时,MayLeakHandler仍被有效对象引用,
于是Activity对应的内存也无法被释放。
为了比较好的理解这个问题,我们看看Handler涉及到的一些源码。
当创建Handler时,最终调用的源码片段如下:
public Handler(Callback callback, boolean async) {
............
//调用sThreadLocal.get(),即从应用主线程获取Looper
mLooper = Looper.myLooper();
............
//获取主线程的MessageQueue
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
当Handler发送消息或Runnable对象时,最终将调用到如下源码:
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
//this指handle,即Msg将持有handler
msg.target = this;
..........
//Msg被加入到MessageQueue中,被MessageQueue持有
return queue.enqueueMessage(msg, uptimeMillis);
}
根据上面的源码可以看出,当定义一个非静态内部类Handler时,
该Handler将被应用主线程的MessageQueue持有;
而Handler又持有了Activity的引用,于是即使Activity界面结束,
若Msg被有被处理掉,MessageQueue将一直持有Activity导致内存泄露。
对于上面那种使用Handler的方式,通常的修改方式是:
public class MainActivity extends AppCompatActivity {
private MayLeakHandler mHandler = new MayLeakHandler(this);
//Runnable也必须变成静态的,否则也会内存泄漏
private static Runnable mRunnable = new Runnable() {
@Override
public void run() {
//.......
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler.postDelayed(mRunnable, 6000);
finish();
}
private static class MayLeakHandler extends Handler {
//如果MayLeakHandler需要访问Activity中的变量,就持有Activity的弱引用
//这样垃圾回收时,就可以清除Activity的内存
private WeakReference<Activity> mActivity;
MayLeakHandler(Activity activity) {
super();
mActivity = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
if (mActivity.get() != null) {
//.............
}
}
}
@Override
protected void onDestroy() {
//最后根据需要需要,在Activity的onDestroy中清除mHandler处理的Message和Runnable
//也可以调用其它接口单独清理msg或runnable
//对于这个例子,Runnable是静态的,所以不需要
//但其它情况msg和runnable可能会持有Activity,所以需要清理
mHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
}
4、匿名内部类引入的泄露
匿名内部类也会持有外部类的引用,
因此与非静态内部类一样,也有可能导致内存泄露,
上面例子中初始定义的匿名Runnable,就会导致这个问题。
比较一般的场景是,如果匿名内部类被异步线程持有,
当异步线程与外部类的生命周期不一致时,就会导致内存泄露。
举例如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Runnable runnable = new Runnable() {
@Override
public void run() {
//.........
}
};
//假设workThread是一个长期运行的HandlerThread
WorkThread workThread = WorkThread.getWorkThread();
Handler handler = new Handler(workThread.getLooper());
handler.post(runnable);
}
}
上面的代码中,定义了一个匿名内部类runnable,该runable对象持有对Activity的引用。
将该runnable对象递交给WorkThread处理时,workThread就会持有该runable对象的引用,进而持有Activity对象。
如果workThread之前在进行某个耗时操作,那么可能Activity结束时,runable对象还未执行完毕,
于是Activity对应的内存没有及时释放,导致内存泄露。
这种类型的问题的解决方法,可能只有将runable写成静态类或单独抽取成一个独立的类。
5、线程相关的内存泄漏
在界面中使用线程对象,稍不注意也会造成内存泄露:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
leakOne();
}
private void leakOne() {
new Thread() {
@Override
public void run() {
while (true) {
SystemClock.sleep(1000);
}
}
}.start();
}
}
很明显,这个问题与匿名内部类引起的内存泄露一样,由于Thread持有对Activity的引用,
同时Thread一直在运行,因此当Activity结束时,对应内存也不会被释放,导致内存泄露的放生。
现在,我们修改一下代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
leakTwo();
}
private void leakTwo() {
new LeakThread().start();
}
private static class LeakThread extends Thread{
@Override
public void run() {
while (true) {
SystemClock.sleep(1000);
}
}
}
}
可以看到,现在Thread变成了一个静态内部类,不再持有对Activity的引用,
因此Activity退出后,对应的内存可以被释放掉。
然而,这段代码还是有问题。
Activity每次创建时,均会创建一个新的永不结束的Thread。
JVM会持有每个运行Thread的引用,因此Activity创建出的Thread将不会被释放掉。
于是,不断的关闭打开Activity,将导致JVM持有的Thread越来越多。
因此上述代码需要修改为:
public class MainActivity extends AppCompatActivity {
private LeakThread mLeakThread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
leakThree();
}
private void leakThree() {
mLeakThread = new LeakThread();
mLeakThread.start();
}
private static class LeakThread extends Thread{
private boolean mRunning = false;
@Override
public void run() {
mRunning = true;
while (mRunning) {
SystemClock.sleep(1000);
}
}
void close() {
mRunning = false;
}
}
@Override
protected void onDestroy() {
mLeakThread.close();
super.onDestroy();
}
}
修改比较简单,就是在Activity结束时,主动停止Thread。
6、资源未关闭造成的内存泄漏
解决最后这一类的内存泄露,主要就是要注意编程细节了。
使用BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源时,
应该在不再使用时,及时关闭或者注销。
四、总结
对Android中的内存泄露就先总结到这里了。
如何避免内存泄露,在上述例子中已经有对应的解决方案了,此处就不做赘述。
总之,代码看到多写的多,自然会养成良好的编程习惯,死记硬背一些规则,效率肯定比较低。
最后提一下检测内存泄露的工具,MAT有很多的资料,此处不做说明了。
有兴趣的话,推荐大家看看LeakCanary,这个开源工具可以极大地节省分析内存泄露的时间。
LeakCanary: 让内存泄露无所遁形
LeakCanary 中文使用说明
看看中文使用说明和对应demo,基本上就能了解如何使用了。