一.什么是内存泄漏
众所周知,为了合理利用内存空间,不再被使用的对象应该被系统回收从而释放内存,这是所有面向对象编程的共识。在java中,有一套完整的GC回收机制来管理上述事务。然而,在实际开发过程中,我们常常遇到在应用程序运行时,符合上述条件的对象无法被回收,长期占用有限的内存空间,这种现象就被称为内存泄漏。
二.内存泄漏产生的影响
要描述内存泄漏产生的影响,我们首先要理解另外一个概念:内存溢出。当初我学艺不精时,常常会混淆这两个概念。所谓内存溢出,就是应用程序占用的内存超过了系统分配的内存,而这会导致应用程序崩溃。上面说到,内存泄漏是因为应该回收的对象无法被回收。那么当一个应用程序存在内存泄漏并且在长时间运行的过程中持续产生内存泄漏,最终占用的内存越来越多,导致内存溢出。由此,我们可以总结:
1.内存泄漏是导致内存溢出的原因之一
2.内存溢出是持续的内存泄漏必然的结果
反映在现象上,就是存在内存泄漏点的应用程序,在运行时容易崩溃,这就是内存泄漏的影响。无疑,内存泄漏会导致用户体验下降,甚至会导致严重的线上事故。
三.内存泄漏的根本原因
前文说到,产生内存泄漏,是因为不再被使用的对象无法被回收。而对象无法被回收,简单地说,是因为它仍然被其他对象强引用(关于对象的回收,大家可以参考java内存回收的可达性分析机制,本文不作深入讨论)。至此,我们可以得出结论,造成内存泄漏的根本原因是应该被回收的对象被不恰当地引用,而这种不恰当的引用一般是由于引用者和被引用者生命周期不一致导致的,下面我们列举一些常见的内存泄漏进行说明
四.常见的内存泄漏
1.集合引起的内存泄漏
被add进集合的对象,会被集合持有引用。随着应用程序的运行,尽管该对象可能成为了“不再使用的对象”,但是由于集合一直没有被回收,所以这个对象也不会被回收,从而产生了内存泄漏。
解决的方法是
1.对于不使用的对象,应该及时从集合中remove。
2.如果觉得一个一个移除增加了代码复杂度,也可以在合适的时机clear集合。
2.静态变量引起的内存泄漏
在Android开发中,有一个非常经典的内存泄漏案例就是静态Context变量
private static Context mContext;
如果将一个Activity赋值给mContext,那么它将一直持有Activity的引用。被static关键字修饰的对象,其生命周期等同于应用程序生命周期,而Activity的生命周期显然更短。这样一来就导致该Activity一直无法被回收,产生内存泄漏。
解决的方法是
1.使用Application的Context
2.如果条件允许,可以使用Activity的弱引用
另一个典型场景是单例模式引起内存泄漏。无论使用哪一种单例模式,instance对象都是被static修饰的。我们创建单例类的时候,要特别注意它的成员变量是否有内存泄漏的风险。
3.非静态内部类引起的内存泄漏
在java中,非静态内部类会持有外部类的引用。如果这个内部类的对象被其他线程引用,或者创建了这个内部类的静态对象,都有可能导致这个对象和外部类的生命周期不一致。从而导致外部类的对象无法被回收,造成内存泄漏。
解决的方法是
1.非必要不使用内部类
2.使用内部类时应使用静态内部类,静态内部类默认不持有外部类的引用
3.对于内部类需要持有外部类的情况,比如当Handler作为Activity的静态内部类时,需要持有Activity的引用来调用Activity的方法。这时应该将Activity的引用声明为弱引用。
4.多线程引起的内存泄漏
多线程特别容易造成任务与发起任务的对象生命周期不一致的情况。例如在Activity中开启子线程处理耗时任务,任务未结束时,Activity可能已经被destroy。所以在多线程编程中,要特别注意线程是否持有外部对象,如果持有,是否有内存泄漏的风险。
5.资源对象引起的内存泄漏
我们开发中常常用到IO流,数据库游标,Bitmap等对象。使用这些类的对象时,要做到“有始有终”。即:打开要有关闭,注册要有注销。
五.总结
一个复杂的系统,内存泄漏的风险也许不止上面列出的情况。不过出现风险的原因万变不离其宗,开发者在日常开发中要养成对此高度的敏感性,时间一长,会养成习惯,待着规避风险的意识去写代码,代码质量自然越来越高