文章目录
概念
内存泄漏(Memory Leak)
内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。说白了就是一个不再使用的对象,本该被回收,但系统回收不了。
内存溢出(Out Of Memory—OOM)
系统已经不能再分配出你所需要的空间,比如你需要100M的空间,系统只剩90M了,这就叫内存溢出。
两者的关系
轻微的内存泄漏没什么影响,但严重的时候会引起OOM,即发生内存泄漏的对象很大,浪费了很多空间,导致系统剩余空间不足以分配给新申请的空间。所以,在我们的日常开发中,一定要避免内存泄漏的情况,但内存泄漏不易察觉(上面说了,内存泄漏不严重时,程序没什么影响,即可以正常运行),下面介绍java和Android中常见的内存泄漏类型。
Java虚拟机的GC(垃圾回收)策略
在了解内存泄漏之前,有必要先理解虚拟机的GC策略。
可达性分析算法
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。Java 虚拟机使用该算法来判断对象是否可被回收。
如上图所示,对象1、2、3、5是可达的,即存活,对象4、6、7是不可达的,尽管对象6、7存在相互引用,但它们都是没办法从GC root开始搜索到的,所以它们都是不可达的,都会被系统回收内存。
引用记数算法
这是另一种回收策略,为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
这种算法有一个致命的缺点,循环引用的对象计数器永远不等于0,比如说上图的对象6、7,它们的引用数都为1,但这两个对象对于程序来说都已经无用了(无法调用,因为没有引用指向它们),系统却无法回收。因此,java虚拟机选用的是可达性分析算法作为GC策略,Android是基于java开发的,所以Android沿用了java 的GC策略。
内存泄漏的原因
通过上面的分析可以知道,不可达的对象一定会被虚拟机回收内存,所以内存泄漏不会发生在不可达的对象上,也就是说,内存泄漏的位置一定发生在可达的对象上。
比如说下面的图,程序后续已经不会再使用对象3了,理论上可以回收它,但由于对象1持有指向对象3的引用,导致系统无法回收对象3,这时对象3就发生内存泄漏。在Android中,可以把对象3比作一个Activity,假设用户退出了该Activity,程序已经用不到该Activity的东西(一些View控件等等,界面都没了怎么用),理论上是可以回收该Activity占用的内存,但某个地方还引用着该Activity,导致回收不了。一个Activity占用的内存还是不容小觑的,可能会产生严重的影响,比如程序崩溃。
内存泄漏的解决方案
知道了内存泄漏的原因,那解决方法就很明显了,只要切断对无用对象的引用就好了。解决方案很简洁,但简洁不代表简单,下面将分析java和Android常见的内存泄漏和具体的解决方案,让大家更深的理解上面的那句话。
Java中的内存泄漏
集合类引起的内存泄漏
ArrayList<Book> bookArrayList = new ArrayList<>();
Book book = new Book( "Android开发艺术探索",58);
bookArrayList.add(book);
// 模拟使用list中的book
Log.d(getClass().getName(), "onCreate: " + bookArrayList.get(0).getBookName());
// 使用完毕后将对象置空
book = null;
上面这段代码看似完美,创建一个book对象使用,使用完后将对象置空,节省空间。但事实是,list还引用着book对象,book对象还是可达,系统无法回收其内存。其过程可以用下图表示。
正确的做法是删除集合对元素的引用
bookArrayList.remove(book);
//或者
bookArrayList.clear();
bookArrayList = null;
static关键字引起的内存泄漏
我们知道用static关键字修饰的变量,它的生命周期和整个程序的生命周期一样,这部分的对象是没办法被回收的(如果不显式地置为null),如果static修饰的对象还引用其他对象,那么引用的这些对象也无法被回收。
单例模式引起的内存泄漏
其实单例模式的隐患就是static关键字引起的,它和上面没有本质的区别。
如下有一个单例类。
public class SingleInstance {
private Book book;
private static SingleInstance instance=new SingleInstance();
private SingleInstance(){
}
public static SingleInstance getInstance(){
return instance;
}
public Book getBook() {
return book;
}
public void setBook(Book book) {
this.book = book;
}
}
Book类
public class Book {
private String bookName;
private int price;
public Book(String bookName, int price) {
this.bookName = bookName;
this.price = price;
SingleInstance.getInstance().setBook(this);
}
// getter and setter
}
调用
Book book = new Book("Android开发艺术探索",60);
book = null;
Log.d(getClass().getName(), "onCreate: " + SingleInstance.getInstance().getBook().getBookName());
可以看到book即使置为null,单例类还是可以调用它,即持有它的引用,导致它无法被回收。现在这个Book类很小,没什么影响,但如果单例类持有的对象很大呢?比如一个Activity,那影响就很大了。
内部类引起的内存泄漏
java中非静态内部类会隐形地持有外部类的引用(不然怎么调用外部类的方法),如果外部类需要被回收,内部类还在使用,此时外部类就发生内存泄漏。关于这类型的例子,在Android中有一个非常经典的场景,handler的使用,下面会详细分析。可通过静态内部类和弱引用搭配解决。
各种连接
数据库连接、文件输入输出流(IO)、网络连接(Socket)等,一定要显式调用它们的close方法,否则永远不会被回收。这种情况下一般都会在try里面去连接,在finally里面释放连接。
注册和解绑
java中经常有registerXXX(Object A)方法,假设B对象调用了这个方法,则可能B对象就持有A的引用,如果此时A需要被回收,但B引用着A,导致A发生内存泄漏。解决方案:B使用完A后要调用解绑方法,一般是unregisterXXX(Object A)。
Android中的内存泄漏
在Android中的内存泄漏例子,本质上也是Java内存泄漏类型中的其中一种,只是用到的类是Andorid特有的而已。
单例类持有Context
在java的内存泄漏类型中提到,如果单例类持有Activity引用的话,会导致Activity无法被释放从而导致内存泄漏。然而,在Android中,Context的使用率是很高的,例如获取资源、跳转页面等都要用到Context,甚至有时候我们需要把Context传到单例类里面才能实现某些需求,可能会写出下面的代码。
public class SingleInstance {
private Context context;
private static SingleInstance instance;
private SingleInstance(Context context){
this.context = context;
}
public static SingleInstance getInstance(Context context){
if (instance == null) {
instance = new SingleInstance(context);
}
return instance;
}
}
//调用
SingleInstance.getInstance(MainActivity.this);
这时MainActivity就释放不了,当MainActivity推出时,便发生内存泄漏。
解决方法
单例类因为用static关键字修饰,所以生命周期跟应用程序一样;我们知道有一个Context的生命周期也是跟应用程序一样的,它就是Application的Context,可以把它传给单例类,从而解决内存泄漏的问题。
Handler
关于Handler的内存泄漏场景,我在另一篇文章中有详细介绍(原因和解决方法),这里直接贴传送门:Android多线程的使用方式
其实并不只是Handler,还有AsyncTask(另一篇文章也有讲到),甚至Thread都会引起内存泄漏,只要是内部类(无论是成员内部类还是匿名内部类)而且涉及到异步操作,就要万分小心。
属性动画引起的内存泄漏
从Android 3.0开始,Google提供了属性动画,属性动画中有一类无限循环的动画,如果在Activity中播放此类动画且没有在onDestroy中去停止动画,那么动画会一直播放下去,尽管已经无法在街面上看到动画效果了,并且这个时候的Activity的View会被动画持有,而View又持有Activity,最终Activity无法释放。下面的动画是无限动画,会泄漏当前Activity,解决方法是在Activity的onDestroy中调用animator.cancel()停止动画。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory_leak);
mButton = findViewById(R.id.start_animation);
animator = ObjectAnimator.ofFloat(mButton,"rotation",0,360).setDuration(2000);
// 无限循环
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
//animator.cancel();
}
属性动画内部也是有一个单例类,大家可以追踪start()和cancel()的源码,其实就是动画类在单例类中注册和解绑。
其他
除了上面这些常见的,还有数据库的游标、广播注册与注销、一些第三方框架使用也需要注意内存泄漏的问题。
总结
内存泄漏的情况是列不完的,这里只是列出常见的内存泄漏的例子,其实它们本质都是一样,万变不离其中,都是可达对象持有无用对象的引用,导致无用对象无法被回收。解决方法是切断所有对其的引用(注意不是单纯的把某个对象设为null,这样只是把某个指向该对象的一个引用切断,如果还有其他引用指向该对象,系统还是回收不了)。