最近最我的机顶盒里有个Android应用老是提示
应用已停止运行
,我查看Log后发现是Java堆内存溢出然后挂掉了。Java不是自动管理内存吗,怎么还会有内存泄漏,是Java虚拟机的垃圾回收机制有问题吗?其实这不能甩锅给Java的垃圾回收机制,都是程序员自己做的孽,一起来了解下吧。
一、必要基础知识
Java 的内存分配介绍
- 方法区(non-heap):编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量;
- 栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存;
- 堆区(heap):通常用来存放 new 出来的对象。由 GC 负责回收。
我们今天主要讨论堆区的内存泄漏。
Java程序中内存泄漏的几种类型
-
使用了C/C++编写的动态库,而动态库里边有内存泄漏。
-
资源类对象忘记释放。
如数据库操作的Cursor对象、流对象等必须手动释放。这些一般对应于操作系统中的文件句柄,泄漏后表现为文件句柄泄漏。
例如:
FileOutputStream fos = newFileOutputStream(new File("test.txt")); //需要自行关闭,否则进程文件句柄泄漏 fos.close();
-
本该释放的对象,由于被其他对象引用导致无法释放。
这是我们主要探讨的泄漏类型。Java不提供释放对象内存的接口,程序员感觉似乎可以撒手不管内存的释放了,但是Java的垃圾回收机制中,被别的对象强引用的都不算是垃圾,所以无法被释放。
我们今天主要讨论第三个:对象被引用导致的内存泄漏。
Java中的引用类型
- 强引用(Strong Reference):JVM 宁愿抛出 OOM,也不会让 GC 回收存在强引用的对象。
- 软引用(Soft Reference) :一个对象只具有软引用,在内存不足时,这个对象才会被 GC 回收。
- 弱引用(weak Reference):在 GC 时,如果一个对象只存在弱引用,那么它将会被回收。
- 虚引用(Phantom Reference):任何时候都可以被 GC 回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。
我们今天主要讨论第一种:对象被强引用导致的内存泄漏。
Java没有析构函数,如何知道Java什么时候释放内存
Java中,没有C/C++里面的free, delete语句,无法直接释放创建的对象。Java剥夺程序员这个权利,所以不会出现C/C++中的内存重复释放问题。既然Java没有接口直接释放内存,那么怎么知道自己创建的对象什么时候被释放呢?
其实在任意一个Java类里重载Object类的finalize方法即可。Java中用 java.lang.System.gc()
方法即可请求虚拟机立马执行一次垃圾回收,把没有被使用的内存释放掉。
public class JavaGcTest {
//重载finalize方法,以便知道对象被虚拟机释放了
@Override
protected void finalize() {
System.out.println("JavaGcTest is destroyed!");
}
public static void main(String[] args) {
System.out.println("create JavaGcTest object");
JavaGcTest j = new JavaGcTest();
//释放对new JavaGcTest()生成的对象的引用
j = null;
System.out.println("execute gc");
//主动向虚拟机申请执行一次垃圾回收
System.gc();
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果
create JavaGcTest object
execute gc
JavaGcTest is destroyed!
从执行结果中可以看到,finalize函数被调用了。所以我们可以用此来调试Java对象的释放过程。
二、常见的内存泄漏及解决方法
单例引起的泄漏
在Android应用开发中,常常需要创建一个单例用于获取进程的Context对象:
public class MainActivity extends AppCompatActivity {
//静态变量用于引用SingleClass类的单例
private static SingleClass sInstance;
private Context mContext;
private SingleClass(Context context){
//直接引用外部Context
mContext = context;
}
public static SingleClass newInstance(Context context){
if(sInstance == null){
sInstance = new SingleClass(context);
}
return sInstance;
}
}
//当外部这么调用时,出问题了
SingleClass single = SingleClass.newInstance(MainActivity.this);
当外部直接将Activity传递给SingleClass时,这个Activity就被SingleClass.sInstance强引用
了,即使Activity执行了finish消耗自己,Activity对象的堆内存资源也得不到释放。
正确的写法:
public class SingleClass {
private static SingleClass sInstance;
private Context mContext;
private SingleClass(Context context){
//引用外部context.getApplicationContext();
mContext = context.getApplicationContext();
}
public static SingleClass newInstance(Context context){
if(sInstance == null){
sInstance = new SingleClass(context);
}
return sInstance;
}
}
修改后,SingleClass单例就引用的是Application对应的Context,而不是Activity了。因为Application的Context本身就是设计为与进程生命周期一致的,所以没有问题,而Activity对象对应的是用户活动界面,在用户离开界面后,必须要销毁的。
我们设计单例的时候,一定要记住这个单例所引用的对象的生命周期是否与单例对象本身一致。
静态成员变量引用造成的泄漏
单例模式的本质是用静态成员变量实现的,而静态成员本身在整个进程结束前都不会释放,所以说所有的静态成员变量很危险,都有可能引起内存泄漏。
例如:
public class MainActivity extends AppCompatActivity {
//弄一个静态成员变量
private static Context context;
@Override
protected void onCreate(Bundle savedInstanceState) {
//让静态变量引用MainActivity.this
context = this;
}
}
从上面例子, context是静态成员变量,强引用MainActivity对象后,MainActivity的内存是无法释放的。如果非要使用这种静态变量,一定要记得将其置空,例如:
@Override
protected void onDestroy() {
//让静态变量释放对MainActivity.this的强引用
context = null;
}
循环引用引起的泄漏
请看下边的例子
class A{
public B b;
}
class B{
public C c;
}
class C{
public A a;
}
public static void main(String[] args)
A a = new A();
B b = new B();
C c = new C();
//A引用B,B引用C,C引用A
a.b = b;
b.c = c;
c.a = a;
//如果要释放a, b,c,一定要将
//a, b, c都设置为null才行!
a = null;
b = null;
c = null;
System.gc();
Thread.sleep(10000000);
}
例子中,要释放循环引用的a,b,c三个对象中的任意一个,必须将a, b, c都设置为null,少一个都释放不了
。例如程序员记得将a = null;b = null;但忘记c=null;三个对象都泄漏了。这就告诫我们千万别写出这种循环引用的代码。
非静态内部类(包括匿名内部类)引起的泄漏
由于这个泄漏比较容易犯,我们举两个例子来说明。
例子一
非静态内部类和匿名内部类都会持有外部对象的强引用,当这些内部类的生命周期比外部类要长时,就容易引起泄漏。
public class MainActivity extends AppCompatActivity {
InnerClass mInner = new InnerClass();
//非静态内部类
private class InnerClass extends Thread {
@Override
public void run() {
//在这里睡眠2小时
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
//这个匿名内部类会持有MainActivity的强引用!
new Thread(new Runnable() {
@Override
public void run() {
//在这里睡眠2小时
}
}).start();
mInner.start();
}
}
上面的例子,即使MainActivity已经销毁了,在虚拟机中因为其匿名内部类和非静态内部类对象mInner还活着,匿名内部类因为持有MainActivity.this的强引用,所以虚拟机必须在这个匿名类对象和mInner睡眠2个小时后才能释放内存。
对于内部生命周期比外部还长的情况,正确的做法我们可以将非静态内部类改为静态内部类,这样就可以避免内部类对MainActivity的强引用了。
例子二
上边的例子,静态内部类与外部没关系,当静态内部类必须持有外部有效引才能干活怎么处理呢?继续看下边的例子:
public class MainActivity extends AppCompatActivity {
//非静态内部类,持有外部类的强引用
private final Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 延迟 5min 发送一个消息
handler.postDelayed(new Runnable() {
//内部会将该 Runable 封装为一个 Message 对象,
//同时将 Message.target 赋值为 handler
@Override
public void run() {
//do something
}
}, 1000 * 60 * 5);
this.finish();
}
}
上面的代码中发送了了一个延时 5 分钟执行的 Message,当该 Activity 退出的时候,延时任务(Message)还在主线程的 MessageQueue 中等待,此时的 Message 持有 Handler 的强引用(创建时通过 Message.target 进行指定),并且由于 Handler 是 MainActivity 的非静态内部类,所以 Handler 会持有一个指向 MainActivity 的强引用,所以虽然此时 MainActivity 调用了 finish 也无法进行内存回收,造成内存泄漏。
正确的做法,可以采用弱引用和静态内部类来实现:
public class MainActivity extends AppCompatActivity {
//声明为静态内部类(避免持有外部类的强引用)
private static final class MyHandler extends Handler{
private final WeakReference<MainActivity> mActivity;
public MyHandler(MainActivity activity){
//使用弱引用
mActivity = new WeakReference<MainActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
HandlerActivity activity = mActivity.get();
//判断 activity 是否为空,以及是否正在被销毁、或者已经被销毁
if (activity == null ||
activity.isFinishing() ||
activity.isDestroyed()) {
//消息队列里边的消息不必再处理了,
//因为activity销毁了
//清空Message,释放Message对MyHandler的强引用
removeCallbacksAndMessages(null);
return;
}
// do something
}
}
private final MyHandler myHandler = new MyHandler(this);
}
根据前边介绍到的WeakReference,在下一次GC时,持有WeakReference引用的对象会被释放掉,所以当Handler很久后才收到Message时,并不会阻碍Activity的内存释放。Handler此时在使用Activity对象时,只需要判断弱引用是否有效,如果无效了则说明Handler也应该需要被销毁了。要销毁Handler,则需要保证消息队列里边的Message被清空了,因为个Message持有Handler的强引用,例子中removeCallbacksAndMessages
接口就是清空消息队列。
总结:
非静态内部类似Android开发时的泄漏的重灾区,Handler,Thread,AsynTask这些阻塞Activity释放的内部类都应该小心。
- 遵循消灭隐性强引用的原则,把内部类设计成静态的。
- 如果静态内部类非要引用外部类,那么也最好使用弱引用(或者软引用)。
使用集合容易导致的泄漏
例如一个数组删除一个元素时,可能忘记将被删除的元素置空导致泄漏:
Data delete(int index) {
//删除数据,可以将数据后边的数据全部前挪一位
....
//剩下最后一个元素,开发者有可能忘记赋值为null导致泄漏
//--size;
//正确的方式应该将引用置空
elementData[--size ] = null;
}
注册了回调后,忘记取消注册导致的泄漏
例如Android中的监听蓝牙事件,如果忘记取消注册的回调,也会导致泄漏。
class MainActivity extends AppCompatActivity {
void xxx() {
SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
//registerListener后,MainActivity.this将会被SensorManager强引用
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
//不使用时,需要取消注册回调,MainActivity无法释放
sensorManager.unregisterListener(this);
}
}
使用系统服务时的泄漏
class MainActivity extends AppCompatActivity {
void checkConnec(Context ctx) {
ConnectivityManager cm = ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
//...
}
@Override
protected void onCreate(Bundle savedInstanceState) {
//这里传递MainActivity.this将导致MainActivity无法被释放!
checkConnec(this);
}
}
据说ConnectivityManager之外,WifiManager, CameraManager, PackageManager这些服务也会出现内存泄漏的问题,我没有具体去跟踪,原因肯定是Activity被强引用了。
正确的做法,在使用系统服务的时候,都应该使用Application的Context:
ConnectivityManager cm = ctx.getApplicationContext()getSystemService(Context.CONNECTIVITY_SERVICE);
其他
不再一一举例,基本上都是该释放的内存由于被可见的或者隐藏的长生命周期的对象强引用导致对象之一无法释放。我们要做的就是找出这些对象,避免它们引用需要释放的对象。
总结
在Java中虽然有虚拟机帮助管理内存,程序员不必直接与进程虚拟内存打交道了,但是程序员仍需谨慎避免内存泄漏的产生,起码避免以下几种泄漏:
- 避免native库泄漏。
- 避免资源型对象泄漏(文件句柄泄漏)。
- 避免写出循环引用,导致的泄漏。
- 避免该释放的对象因长生命周期的对象强引用而无法释放。(静态成员变量强引用,非静态内部类对象强引用)
从上边的例子来看,Java易犯的内存泄露的错误很大原因是对Java虚拟机的不了解。为了加深对JVM的理解,建议大家直接看《深入理解Java虚拟机》一书,我已经给大家找好了高清带目录标签的版本:
关注下面的公众号,然后回复"深入理解Java虚拟机"即可获取。