全面总结Android内存泄漏(上)

参考资料:
Android 内存泄漏总结
Android内存泄漏小谈

你真的懂 Java 的内存管理和引用类型吗?
Android 性能优化:手把手带你全面了解内存泄露
[贝聊科技]使用Android Studio和MAT进行内存泄漏分析
【Android 性能优化】—— 详解内存优化的来龙去脉
Android 关于内存泄露,你必须了解的东西
Android性能优化系列之内存优化
Android 内存泄漏案例和解析
Android 内存泄漏分析心得
Android开发常见的Activity中内存泄漏及解决办法
Android内存泄漏查找和解决
Android 内存泄漏全解
彻底搞懂Java内存泄漏
简析Android的垃圾回收与内存泄露
Android应用内存泄漏的定位、分析与解决策略
Android 性能优化 - 彻底解决内存泄漏
利用Android Studio、MAT对Android进行内存泄漏检测
Android内存泄漏学习笔记

内存泄漏是Android中一个很常见的问题, 所以我决定尽可能全面的总结一下内存泄漏。因为篇幅过长,分两篇。第一篇总结常见的内存泄漏和解决方案,第二篇介绍常用的内存泄漏的检测方法和工具。

Java内存分配策略

Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。

  • 静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
  • 栈区:方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。
  • 堆区: 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

堆与栈的区别

在方法体内定义的(局部变量)一些基本类型的变量对象的引用变量都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用来存放所有由new 创建的对象(包括该对象其中的所有成员变量)数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

举个例子

1
2
3
4
5
6
7
8
9
10
11
public class Sample {
int s1 = 0;
Sample mSample1 = new Sample();

public void method() {
int s2 = 1;
Sample mSample2 = new Sample();
}
}

Sample mSample3 = new Sample();

Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。
mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。
结论:
局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。

Java的四种引用

  • 强引用(Strong Reference): 指在程序代码之中普遍存在的,类似 Object obj = new Object() 这类的引用,只要强引用还存在,垃圾回收器「永远」不会回收掉被引用的对象
  • 软引用(Soft Reference): 用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常
  • 弱引用(Weak Reference): 用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
  • 虚引用(Phantom Reference): 也被称为幽灵引用或幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

用一张表来总结java的四种引用

类型 回收时机 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 联合ReferenceQueue构造/有效期短/占内存大/生命周期长的对象的二级高速缓存器(内存不足时才清空) 内存不足时终止
弱引用 在垃圾回收时 联合ReferenceQueue构造/有效期短/占内存大/生命周期长的对象的一级高速缓存器(系统发生gc时则清空) gc运行后终止
虚引用 在垃圾回收时 联合ReferenceQueue来跟踪对象被垃圾回收器回收的活动 gc运行后终止

代码测试一下

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Main {

public static void main(String[] args) throws InterruptedException {
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
System.out.println("软引用:");
testSoftReference(referenceQueue);
System.out.println("弱引用:");
testWeakReference(referenceQueue);
System.out.println("虚引用:");
testPhantomReference(referenceQueue);
}

private static void testSoftReference(ReferenceQueue<Object> referenceQueue)
throws InterruptedException {
Object objSoft = new Object();
SoftReference<Object> softReference = new SoftReference<>(objSoft, referenceQueue);
System.out.println("GC前获取:" + softReference.get());
objSoft = null;
System.gc();
Thread.sleep(1000);
System.out.println("GC后获取:" + softReference.get());
System.out.println("队列中的结果:" + referenceQueue.poll());
}

private static void testWeakReference(ReferenceQueue<Object> referenceQueue)
throws InterruptedException {
Object objWeak = new Object();
WeakReference<Object> weakReference = new WeakReference<>(objWeak, referenceQueue);
System.out.println("GC前获取:" + weakReference.get());
objWeak = null;
System.gc();
Thread.sleep(1000);
System.out.println("GC后获取:" + weakReference.get());
System.out.println("队列中的结果:" + referenceQueue.poll());
}

private static void testPhantomReference(ReferenceQueue<Object> referenceQueue)
throws InterruptedException {
Object objPhan = new Object();
PhantomReference<Object> phantomReference = new PhantomReference<>(objPhan, referenceQueue);
System.out.println("GC前获取:" + phantomReference.get());
objPhan = null;
System.gc();
// 此处的区别是当objPhan的内存被gc回收之前虚引用就会被加入到ReferenceQueue队列中,
// 其他的引用都为当引用被gc掉时候,引用会加入到ReferenceQueue中
Thread.sleep(1000);
System.out.println("GC后获取:" + phantomReference.get());
System.out.println("队列中的结果:" + referenceQueue.poll());
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
软引用:
GC前获取:java.lang.Object@2503dbd3
GC后获取:java.lang.Object@2503dbd3
队列中的结果:null

弱引用:
GC前获取:java.lang.Object@4b67cf4d
GC后获取:null
队列中的结果:java.lang.ref.WeakReference@7ea987ac

虚引用:
GC前获取:null
GC后获取:null
队列中的结果:java.lang.ref.PhantomReference@12a3a380

什么是内存泄漏

简单来说就是当你不再需要某个实例后,但是这个对象却仍然被引用,导致系统无法进行回收, 这个情况就叫做内存泄漏(Memory Leak)。
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常GC采用有向图的方式记录并管理堆中的所有对象,通过这种方式确定哪些对象时“可达”,哪些对象时“不可达”。当对象不可达的时候,即对象不再被引用的时候,就会被垃圾回收。
举个例子:

在第六行的时候,o2改变了指向,Obj2就不再引用main的了,即他它们是不可达的,Obj2就可能在下次的GC中被回收

内存泄漏的原因

出现内存泄露的原因仅仅是外部人为原因 = 无意识地持有对象引用,使得 持有引用者的生命周期 > 被引用者的生命周期

泄漏的源头

这里暂时分为三类

  • 自身编码引起
    由项目开发人员自身的编码造成。
  • 第三方代码引起
    这里的第三方代码包含两类:第三方非开源的SDK和开源的第三方框架。
  • 系统原因
    由 Android 系统自身造成的泄漏,如像 WebView 、 InputMethodManager 等引起的问题,还有某些第三方 ROM 存在的问题。

常见的内存泄漏

下面举例说明常见的内存泄漏

静态变量造成的内存泄漏

静态变量的生命周期,起始于类的加载,终止于类的释放。对于 Android 而言,程序也是从一个 main 方法进入,开始了主线程的工作,如果一个类在主线程或旁枝中被使用到,它就会被加载,反过来说,假如一个类存在于我们的项目中,但它从未被我们使用过,算是个孤岛,这时它是没有被加载的。一旦被加载,只有等到我们的 Android 应用进程结束它才会被卸载。
举个例子:

1
2
3
4
5
6
7
8
9
10
11
public class LeakStaticActivity extends AppCompatActivity {

private static Context sContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak_static);
sContext = this;
}

}

看一下LeakCanary检测出来的内存泄漏

这样的代码会导致当这个 Activity 结束的时候,sContext 仍然持有它的引用,致使 Activity 无法回收。同样的,如果一个 Activity 的静态 field 变量内部(比如一个静态的View)获得了当前 Activity 的引用,比如我们经常会把 this 传给 View 之类的对象,这个对象若是静态的,并且没有在 Activity 生命周期结束之前置空的话,也会导致同样的问题。

看一下静态View造成的内存泄漏:

1
2
3
4
5
6
7
8
9
10
11
public class StaticViewLeakActivity extends AppCompatActivity {

private static View sView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_static_view_leak);
sView = findViewById(R.id.tv);

}
}


View一旦被加载到界面中将会持有一个Context对象的引用,在这个例子中,这个context对象是我们的Activity,声明一个静态变量引用这个View,也就引用了activity,所以当activity生命周期结束了,静态View没有清除掉,还持有activity的引用,因此内存泄漏了。
解决办法:

  1. 在这个 Activity 的 onDestroy 时将 sContext 的值置空
  2. 避免使用静态变量这样的写法()
  3. 尽量避免 Static 成员变量引用资源耗费过多的实例(如 Context), 若需引用 Context,则尽量使用Applicaiton的Context
  4. 使用弱引用代替强引用。

单例造成的泄漏

单例造成的原因其实和上面的静态变量一样
举个例子:

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
30
public class AppManager {
private Context mContext;

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

private volatile static AppManager INSTANCE;

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

public class SingletonLeakActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_singleton_leak);
AppManager.getInstance(this);
}
}


可以看到AppManager这个对象中的变量mContext引用着SingletonLeakActivity这个对象, 然后这个AppManager对象又是一个静态的的类变量, 生命周期和等于应用的生命周期, 导致activityj结束生命周期需要被销毁时而无法被回收。
解决办法:

1
2
3
private AppManager(Context context) {
mContext = context.getApplicationContext();
}

非静态内部类和匿名内部类造成内存泄漏

handler造成的内存泄漏

最为常见的就是非静态的handler造成的内存泄漏了。先看看原因。

在 Java 中,非静态内部类包括匿名内部类,比如非静态的Handler会引用外部类对象this(比如 Activity)
而message又会引用handler, 看一下Handler的代码

1
2
3
4
5
6
7
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

可以看到Message有一个target字段引用着handler。
假如Handler发送的Message尚未被处理(比如一个延时执行的Message),就会一直存在于对应线程的MessageQueue中。在这段时间内,该Message会被MessageQueue一直持有,而Message又引用着Handler, Handler又引用着Activity,导致Activity在finish的时候无法被回收。
解决方案:

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
30
31
32
public class HandlerLeakActivity extends AppCompatActivity {

private TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_leak);
}

StaticHandler mHandler = new StaticHandler(this);

static class StaticHandler extends Handler {
private WeakReference<HandlerLeakActivity> reference;

StaticHandler(HandlerLeakActivity activity) {
reference = new WeakReference<>(activity);
}

@Override
public void handleMessage(Message msg) {
HandlerLeakActivity activity = reference.get();
if (activity != null) {
activity.getTextView().setText("测试");
}
}
}

private TextView getTextView() {
return mTextView;
}
}

虽然我们解决了Activity的内存泄漏问题,但是经过Handler发送的延时消息还在MessageQueue中,Looper也在等待处理消息,所以我们要在Activity销毁的时候处理掉队列中的消息。

1
2
3
4
5
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}

非静态内部类/匿名类

先看看非静态内部类(non static inner class)和 静态内部类(static inner class)之间的区别

类型 与外部class引用关系 被调用时是否需要外部实例 能否调用外部class 中的变量和方法 生命周期
静态内部类 如果没有传入参数,就没有引用关系 不需要 不能 自主的生命周期
非静态内部类 自动获得强引用 需要 依赖于外部类,甚至比外部类更长

可以看到非静态内部类自动获得外部类的强引用,而且它的生命周期甚至比外部类更长,这便埋下了内存泄露的隐患。如果一个 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
25
26
27
28
29
30
31
32
33
34
public class AsyncTaskActivity extends AppCompatActivity {
private static final String TAG = "AsyncTaskActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_task);
View button = findViewById(R.id.async_task);
button.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
startAsyncTask();
}
});
}



@SuppressLint("StaticFieldLeak")
void startAsyncTask() {
// This async task is an anonymous class and therefore has a hidden reference to the outer
// class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation),
// the activity instance will leak.
new AsyncTask<Void, Void, Void>() {
@Override protected Void doInBackground(Void... params) {
Log.d(TAG, "开始内存泄漏");
// Do some slow work in background
SystemClock.sleep(20000);
return null;
}
}.execute();
// 屏幕旋转或Activity在后台被系统杀掉等情况会导致Activity的重新创建,之前运行的AsyncTask会持有一个之前Activity的引用,
// 这个引用已经无效,这时调用onPostExecute()再去更新界面将不再生效。
}
}


可以看到我们在 Activity 中继承 AsyncTask 自定义了一个非静态内部类,在 doInbackground() 方法中做了耗时的操作,然后在 onCreate() 中启动 MyAsyncTask。如果在耗时操作结束之前,Activity 被销毁了,这个线程在Activity销毁后还一直在后台执行,那这个线程会继续持有这个Activity的引用从而不会被GC回收,直到线程执行完成,这时候便会产生内存泄露。
解决方案1:使用非静态的内部类

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

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new MyAscnyTask().execute();
}

static class MyAscnyTask extends AsyncTask<Void, Integer, String>{
@Override
protected String doInBackground(Void... params) {
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "";
}
}
}

解决方案2:
当外部类结束生命周期时,强制结束线程。例如AsyncTask.cancel(), Thread.stop()或者Thread.interrupt()(但是一般建议最好不要这么做,有时会有bug)。

集合类

集合类添加元素后,仍引用着集合元素对象,导致该集合元素对象不可被回收,从而导致内存泄漏
举个例子:

1
2
3
4
5
6
static List<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Object obj = new Object();
objectList.add(obj);
obj = null;
}

在这个例子中,循环多次将 new 出来的对象放入一个静态的集合中,因为静态变量的生命周期和应用程序一致,而且他们所引用的对象 Object 也不能释放,这样便造成了内存泄露。
解决方法:
在集合元素使用之后从集合中删除,等所有元素都使用完之后,将集合置空。

1
2
objectList.clear();
objectList = null;

WebView 的泄漏

Android 中的 WebView 存在很大的兼容性问题。WebView 解析网页时会申请 Native 堆内存用于保存页面元素,当页面较复杂时会有很大的内存占用。如果页面包含图片,内存占用会更严重。并且打开新页面时,为了能快速回退,之前页面占用的内存也不会释放。有时浏览十几个网页,都会占用几百兆的内存。这样加载网页较多时,会导致系统不堪重负,最终强制关闭应用,也就是出现应用闪退或重启。
由于占用的都是 Native 堆内存,所以实际占用的内存大小不会显示在常用的 DDMS Heap 工具中(这里看到的只是 Java 虚拟机分配的内存,一般即使 Native 堆内存已经占用了几百兆,这里显示的还只是几兆或十几兆)。只有使用 adb shell 中的一些命令比如 dumpsys meminfo 包名,或者在程序中使用 Debug.getNativeHeapSize() 才能看到。
据说由于 WebView 的一个 BUG,即使它所在的 Activity(或者 Service) 结束也就是 onDestroy() 之后,或者直接调用 WebView.destroy() 之后,它所占用这些内存也不会被释放。
解决这个问题最直接的方法是:把使用了 WebView 的 Activity(或者 Service) 放在单独的进程里,通过 AIDL 与主进程进行通信。然后在检测到应用占用内存过大有可能被系统干掉,或者它所在的 Activity(或者 Service) 结束后,调用 System.exit(0),主动 Kill 掉进程。或者可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。由于系统的内存分配是以进程为准的,进程关闭后,系统会自动回收所有内存。
关于WebView的更多内容,请参见:Android WebView Memory Leak WebView 内存泄漏

不需要用的监听未移除产生的内存泄漏

例子1:

1
2
3
4
5
6
7
8
9
tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean b) {
//监听view的加载,view加载出来的时候,计算他的宽高等。

//计算完后,一定要移除这个监听
tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
}
});

例子2:

1
2
3
4
5
SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
// 不需要用的时候记得移除监听
sensorManager.unregisterListener(listener);

属性动画造成内存泄漏

另外当我们使用属性动画,我们需要调用一些方法将动画停止,特别是无限循环的动画,否则也会造成内存泄漏,好在使用 View 动画并不会出现内存泄漏,估计 View 内部有进行释放和停止。

RxJava 使用不当造成内存泄漏

对于异步的操作,如果没有及时取消订阅,就会造成内存泄漏:

1
2
3
4
5
6
Observable.interval(1, TimeUnit.SECONDS)
.subscribe(new Action1<Long>() {
@Override public void call(Long aLong) {
// pass
}
});

同样是匿名内部类造成的引用没法被释放,使得如果在 Activity 中使用就会导致它无法被回收,即使我们的 Action1 看起来什么也没有做。
解决方案:接收 subscribe 返回的 Subscription 对象,在 Activity onDestroy 的时候将其取消订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LeakActivity extends AppCompatActivity {
private Subscription mSubscription;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
mSubscription = Observable.interval(1, TimeUnit.SECONDS)
.subscribe(new Action1<Long>() {
@Override public void call(Long aLong) {
// pass
}
});
}
@Override protected void onDestroy() {
super.onDestroy();
mSubscription.unsubscribe();
}
}

资源未关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File, Cursor,Stream,Bitmap, EventBus等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

下期:总结一些常用的内存泄漏的检测方案

0%