目录
一、内存泄漏的概念
Java虚拟机中某块内存(一般是对象)应当被回收,但是因为某些原因(一般是程序员水平不足)无法被垃圾回收器回收,就叫做内存泄漏。
注意无法回收不等于内存泄漏, 用逻辑学表示就是内存泄漏 → 无法回收,内存泄漏是无法回收的充分条件,无法回收是内存泄漏的必要条件。内存泄漏了肯定是无法回收,但是无法回收不一定就是内存泄漏。比如定义一个强引用,指向一个对象,这个对象到处都在用,它不能回收,但是这不叫做内存泄漏。一定是应该要回收的,但是错误的被引用导致无法回收才叫内存泄漏!
而且,内存泄漏有时间上的相对性。在某一段时间内,可能某个对象意外引用,导致GC无法回收,但是过了某个时间节点,可能对象又可以被回收。但是短时间的内存泄漏堆积,也有引发OOM的危险。
想要彻底弄明白内存泄漏,你需要补充一下以下几点知识
1) Java虚拟机及GC的原理
https://www.jianshu.com/p/d6b7681dd44c
记住几个重要的点:
# 垃圾回收器回收的目标是堆内存中的对象
# 对象回收的条件是对象不再被任何变量引用
# 静态变量本身不会被回收,静态变量所引用的对象是可以回收的。
# 静态变量存储在方法区,不在堆内存,所以不存在回收一说。其所在类,只是便于去访问这个静态变量而已。
2)匿名内部类
https://www.cnblogs.com/wuhenzhidu/p/anonymous.html
所谓匿名内部类,就是在父类Outter里new Inner(){},注意一定要有{},不一定非要复写内部类Inner 的方法。如果只是new Inner(),就不算作是内部类了。
# 匿名内部类持有外部类的强引用
二、内存泄漏的原因
堆内存中的长生命周期对象持有短生命周期对象的强/软引用
,尽管短生命周期的对象已经不再需要了,但是长生命周期对象持有它的引用而导致不能被回收,这就是Java内存泄露的根本原因。
注意持有引用的方向,是长生命周期的对象持有短生命周期对象的引用,导致短生命周期对象无法回收。
所谓的长生命周期,首先我们会联想到static,其实一个无限循环Thread也算作是一个长的生命周期。
三、如何发现内存泄漏
参考博文 https://blog.csdn.net/u012760183/article/details/52068490
内存泄漏到一定极限,如果程序继续申请内存,内存不够用就会抛出OOM异常,导致程序崩溃。注意触发OOM的地方,不一定就是内存泄漏的地方,不要错误地把目标锁定在OOM的地方。发现内存泄漏可以运用一定的工具和手段:
1)Android Studio Profiler工具
https://blog.csdn.net/u010838555/article/details/96483705
如果反复测试程序某个过程,Totals只增不减,强制GC之后仍然没有下降,就说明内存泄漏了。 强制GC之后 dump出内存,如果发现本应该被回收的对象仍然在内存里,那就说明这个对象已经内存泄漏了。
2)Android-LeakCanary
https://www.jianshu.com/p/61860529ee1b
https://blog.csdn.net/wyh_healer/article/details/60961109
LeakCanary 大致框架和实现原理: https://blog.csdn.net/adarcy/article/details/82055945
四、常见的内存泄漏形式
遵循长生命周期引用短生命周期的原则定位
1.静态变量
静态变量(任意的类型)引用了对象,如果业务结束了不需要再使用这些对象,可以把变量清空(容器)或者直接将变量置为null。
2.内部类
1)非静态内部类
https://blog.csdn.net/qq_41991743/article/details/89370840
非静态内部类对象一定会持有其外部类对象的隐式引用
如果非静态内部类对象被某一个变量引用,那么这个内部类的外部类对象也会被这个变量引用。假如这个变量只想持有内部类对象的引用,但是却意外的持有了内部类的外部类对象的引用。如果这个变量为静态变量一直存在,不作清空处理,则认为外部类对象内存泄漏。
举个例子:
如果Activity里定义了一个内部类,定义了一个静态成员变量,静态成员变量引用了内部类对象,如果Activity销毁,但是静态成员变量没有清空,那么这个Activity就内存泄漏了。
ublic class StaticObjectTestActivity extends AppCompatActivity {
private static final String TAG = "StaticObjectTestActivit";
private static BigObj bigObjPub;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_static_object_test);
System.out.println("I'm activity " + this);
bigObjPub = new BigObj();
}
@Override
protected void onDestroy() {
super.onDestroy();
// bigObjPub = null; //此处若将静态变量bigObjPub置为null,则gc可以回收。
}
class BigObj {
private final byte[] bytes;
public BigObj() {
bytes = new byte[1024 * 1024 * 50];
}
@Override
protected void finalize() throws Throwable {
System.out.println("I'm finalize");
super.finalize();
}
}
}
可以看到StaticObjectTestActivity退出后会有一次泄漏,就是因为内部的静态成员变量bigObjPub引用了StaticObjectTestActivity 的内部类BigObj。

非静态内部类本身不一定会导致外部类内存泄漏,一旦内部类对象被静态变量引用,或者被长生命周期的对象引用(如静态变量指向的对象等),或者其自身的生命周期很长(如new 一个Thread sleep很久)都会导致内存泄漏。
2)静态内部类
静态内部类正好是对付非静态内部类内存泄漏的克星,因为它不存在对外部类的隐式引用。它只是定义在外部类里,受外部类的访问限制。
3.单例模式导致的内存泄漏
"单例"其实就是一个静态变量引用的对象,这个对象的生命周期就特别长,如果对象里引用了某个对象(如Context)等,就会造成这个对象无法被回收。
4.静态方法传入对象(Context)
静态方法每调用一次,参数传入Context,但是静态方法所在的类不去声明static变量引用这个Context对象,如果Context每次都是新的对象,那么内存中这个Context对象个数就会一直累加。但是,这种情况,当GC发生时,这些对象可以被自动回收!
#android常见的会引用Context的静态方法API
#Toast
Toast.makeText(this, "1", Toast.LENGTH_SHORT).show();
4.Handler导致Activity内存泄漏
很多人可能都听说过在Activity里使用Handler会导致内存泄漏,但是具体是什么情况下会导致内存泄漏?是在Activity里声明Handler就会导致内存泄漏吗,答案是NO。Handler内存泄漏的本质原因是Handler被引用了,而往往这些引用不明显。
参照https://blog.csdn.net/xzw00/article/details/50156471 我写了几个Demo测试了一下
#1)Handler被MessageQueue引用导致内存泄漏
public class HandlerLeakTestActivity extends AppCompatActivity {
private static final String TAG = "HandlerLeakTestActivity";
protected static final int MSG_TEST = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_leak_test);
Log.d(TAG, "onCreate: " + "I'm activity " + this);
mHandler.sendEmptyMessage(MSG_TEST);
finish();
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
switch (msg.what) {
case MSG_TEST:
Log.w(TAG, "Handle the test message!");
mHandler.sendEmptyMessageDelayed(MSG_TEST, 5000);
break;
default:
break;
}
super.handleMessage(msg);
}
};
}
不断的开启关闭HandlerLeakTestActivity,然后强制GC,发现
多个HandlerLeakTestActivity 及Handler对象仍然驻留在内存中。
把mHandler.sendEmptyMessageDelayed(MSG_TEST, 5000);这行代码去掉,再
开启关闭HandlerLeakTestActivity , 发现多个HandlerLeakTestActivity 及Handler对象驻留在内存中。 但是强制GC,这些对象可以自动回收。也就是说handler的postDelay方法会导致Activity内存泄漏,在 https://www.cnblogs.com/aimr/p/5217918.html 这篇文章说明的很清楚:
如果你执行了Handler的postDelayed()方法,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,那么在你设定的delay到达之前,会有一条MessageQueue -> Message -> Handler -> Activity的链,导致你的Activity被持有引用而无法被回收。
上面的作者阐述的观点: handler的postDelay方法会导致Activity内存泄漏 有一定的片面性,上面
case MSG_TEST:
Log.w(TAG, "Handle the test message!");
mHandler.sendEmptyMessageDelayed(MSG_TEST, 5000);
break;
会导致Handler不停的发送MSG_TEST这个消息,从而形成
MessageQueue -> Message -> Handler -> Activity的链 ,导致Activity无法被回收。
对上面的代码做一下改动:
#改动1:让发送到一定数目消息后,停止消息的发送,发现停止消息发送之后,强制GC可以回收Activity及Handler对象。
public class InnerHandlerTestActivity extends AppCompatActivity {
private static final String TAG = "InnerHandlerTestAct";
protected static final int MSG_TEST = 1;
private int sendCount = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple_handler_test);
Log.d(TAG, "onCreate: " + "I'm activity " + this);
mHandler.sendEmptyMessage(MSG_TEST);
finish();
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
switch (msg.what) {
case MSG_TEST:
if(sendCount > 10){
return;
}
Log.w(TAG, "Handle the test message!");
mHandler.sendEmptyMessageDelayed(MSG_TEST, 1000);
sendCount++;
break;
default:
break;
}
super.handleMessage(msg);
}
};
}
所以,Handler不停地发送消息让MessageQueue 一直持有Handler引用,导致内存泄漏。
#改动2: 就用sendEmptyMessageDelayed稍微延时长点发个消息,发现只有过了延时时间30s之后才能强制GC回收Activity及Handler对象。
public class InnerClassHandlerTestActivity extends AppCompatActivity {
private static final String TAG = "InnerHandlerTestAct";
protected static final int MSG_TEST = 1;
private int sendCount = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_simple_handler_test);
Log.d(TAG, "onCreate: " + "I'm activity " + this);
mHandler.sendEmptyMessageDelayed(MSG_TEST,30 * 1000);
finish();
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
switch (msg.what) {
case MSG_TEST:
break;
default:
break;
}
super.handleMessage(msg);
}
};
}
所以,Handler只要消息没有处理完,就会导致内存泄漏。
#2)Handler被Thread引用导致内存泄漏
下面是Android惯用的刷新UI的写法,在子线程里做耗时操作,然后通过handler发送消息去刷新UI。
public class ThreadHandlerTestActivity extends AppCompatActivity {
private static final String TAG = "InnerHandlerTestAct";
protected static final int MSG_TEST = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread_handler_test);
new Thread(){
@Override
public void run() {
try {
Thread.sleep(30*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mHandler.sendEmptyMessage(MSG_TEST);
}
}.start();
finish();
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
switch (msg.what) {
case MSG_TEST:
Log.w(TAG, "Handle the test message!");
// mHandler.sendEmptyMessageDelayed(MSG_TEST, 5000);
break;
default:
break;
}
super.handleMessage(msg);
}
};
}
Thread 30秒的耗时操作,虽然最后才调用mHandler.sendEmptyMessage(MSG_TEST),但是引用是自Thread创建就开始,到Thread的run方法执行完毕,Thread销毁之后引用才结束。所以在30秒以内,强制GC,Activity、Handler不会被回收。
#3)非内部类的Handler被Thread引用也会导致内存泄漏

如上图,我把Handler单独定义成一个类,不定义在Activity内部。30s之内,MyPubHandler仍然会持有Activity,强制GC也无法回收,但是过了30s之后,强制GC就能将Activity和Handler回收。
什么情况?非匿名内部类的Handler也会导致Activity内存泄漏?仔细看handler的postDelayed方法,创建了一个Runnable的匿名对象,形成了MessageQueue -> Message -> Handler ->Runnable-> Activity的链 。
那么我换个handler的消息方法

这样,30s之内,强制GC会回收Activity,30之后,Handler消息处理完才能能回收。
所以,Handler的内存泄漏应该分为2段来理解:
1)Handler存在待处理的消息,会有 MessageQueue -> Message -> Handler 的引用,导致Handler无法回收。
2)Handler如果定义成匿名内部类或者post产生了一个匿名Runnable,如果Handler无法回收,会持有Activity的引用,导致Activity也无法回收。
#Handler内存泄漏的解决办法
1)最简单有效且最正规的办法就是在Activity的onDestroy方法里调用
mHandler.removeCallbacksAndMessages(null);
但是上面这个方法也不一定完全能保证Handler可以被回收,比如:

handler消息里又搞了一个匿名内部类Thread,Thread永远不会结束,即使调用了mHandler.removeCallbacksAndMessages(null);也避免不了Handler内存泄漏。

GC之后,发现Thread->Handler->Activity,导致无法回收。
2)如果Handler非要定义成匿名内部类,可以定义成静态变量。
#静态的Handler用完之后,记得要置为null,否则这个Handler实例会常驻内存,GC也无法回收,直到应用程序被杀死。Handler置为null,GC也不会立马被回收,只有Handler的消息全部处理完毕才会回收。
#Handler定义成static了,那么它更新View的时候,View启不是也要定义成static?
https://www.cnblogs.com/punkisnotdead/p/4943260.html
上面这篇文章提到了将activity通过弱引用的方式传入到静态的Handler中
0 条评论