内存泄露总结
内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏的问题简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致GC不能回收。
Java内存分配策略
Java程序运行时的内存分配策略共有三种,分别是静态分配、栈式分配、堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(方法区)、栈区和堆区。
静态存储区(方法区):主要存放静态数据、全局static数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
栈区:当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆区:又称动态内存分配,通常就是指在程序运行时直接new出来的内存,也就是对象的实例。这部分内存在不使用时将会有Java垃圾回收器来负责回收。
栈和堆的区别
在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。
堆内存用来存放所有由new创建的对象(包括该对象中的所有成员变量)和数组。在堆中分配的内存,将由垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。
举个例子:
public class Sample{
int s1 = 0;
Sample mSample1 = new Sample();
public void method(){
int s2 = 1;
Sample mSample2 = new Sample();独立8斤
}
}
Sample mSample3 = new Sample();
Sample类的局部变量s2和引用变量mSample2都存在于栈中,但是mSample2指向的对象是存在于堆中的。mSample3指向的对象实体存放在堆上,包括这个对象的所有成员变量s1和mSample1,而它自己存在于栈中。
结论:
局部变量的基本数据类型和引用存储在栈中,引用的对象实体存储在堆中。--因为他们属于方法中的变量,生命周期随着方法而结束。
成员变量全部储存在堆中(包括基本数据类型,引用和引用对象的实体)--因为他们属于类,类对象终究是要被new出来使用的。
Java是如何管理内存?
Java的内存管理就是对象的分配和释放问题。在Java中,程序员通过关键字new为每个对象申请内存空间(基本对象除外),所有的对象都在堆(Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在Java中,内存的分配是由程序完成的,而内存的释放,是由GC完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,他也加重了JVM的工作。这也是Java程序运行速度较慢的原因之一。因为,GC为了能够正确释放对象,GC必须监控每个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。
监视对象状态是为了更加准确地、及时地释放对象,而是释放对象的根本原则就是该对象不再被引用。
什么是Java中的内存泄漏
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与之相连;其次,这些对象是无用的,即程序以后不会再使用这些对象,如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏。这些对象不会被GC所回收,然而它却占用内存。
在C++中,内存泄漏的范围更大一些,有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远不会回收回来。在Java中,这些不可达的对象都由GC来负责回收,因此程序员不需要考虑这部分的内存泄漏。
对于程序员来说,GC基本是透明的不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义,该函数不保证JVM的垃圾回收器一定执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程优先级别较低。JVM调用GC的策略也是有很多种,有的是内存使用达到一定程度时,GC才开始工作,有的是定时执行的,有的是平缓执行GC,有的是中断式执行GC,但是通常来说,我们不需要关心这些,除非在一些特定的场合,GC的执行影响应用程序的性能,例如基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。
同样给出一个Java内存泄漏的典型例子。
Vector v = new Vector(10); for(int i = 1;i
详解Java中的内存泄漏
1. Java内存回收机制
不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址。Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的。GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题。在Java语言中,判断一个内存空间是否符合垃圾收集标准有两个:一个是给对象赋予了空值null,以下在没有调用过,另一个是给对象赋予了新值,这样就重新分配了内存空间。
2. Java内存泄漏引起的原因
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时的释放,从而造成内存空间的浪费称为内存泄漏。内存泄漏有时不严重不易察觉,这样开发者就不知道存在内存泄漏,但有时候也会很严重,会提示你Out of Memory。
Java内存泄漏的根本原因是什么呢?长生命周期的对象持有段生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。具体主要有如下几大类:
(1)静态集合类引起的内存泄漏
像HashMap、Vector等的使用最容易出现内存泄漏,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们将一致被Vector等引用着。
static Vector v = new Vector(10); for(int i=0;i
(2)当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
例如:
public static void main(String[] args){ Set set = new HashSet(); Person p1 = new Person("唐僧","pwd1",25); Person p2 = new Person("孙悟空","pwd2",26); Person p3 = new Person("猪八戒","pwd3",27); set.add(p1); set.add(p2); set.add(p3); System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素! p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变 set.remove(p3); //此时remove不掉,造成内存泄漏 set.add(p3); //重新添加,居然添加成功 System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素! for (Person person : set){ System.out.println(person); } }
(3)监听器
在java编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会
(4)各种连接
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显示的调用了其close()方法将其连接关闭,否则是不会自动被GC回收的。由于ResultSet和Statement对象可以不进行显示回收,但Connection一定要显示回收,因为Connection对象在任何时候都无法自动回收,而Connection一旦回收,ResultSet和Statement对象就会立即为Null。但是如果使用连接池,情况就不一样了,除了要显示地关闭连接,还必须显示地关闭ResultSet和Statement对象(关闭其中一个,另一个也会自动关闭),否则就会造成大量的Statement无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去连接,在finally里面释放连接。
(5)内部类和外部模块的引用
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A负责A模块,调用了B模块的一个方法如:public void registerMsg(Object o);这种调用就要非常的小心,传入了一个对象,很可能模块B就保持了该对象的引用,这时候就需要注意模块B是否提供相应的操作去除引用。
(6)单例模式
不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏,考虑下面的例子:
class A{ public A(){ B.getInstance().setA(this); } .... } //B类采用单例模式 class B{ private A a; private static B instance=new B(); public B(){} public static B getInstance(){ return instance; } public void setA(A a){ this.a=a; } //getter... }
显然B采用singleton模式,它持有一个A对象的引用,而这个A类的对象将不会被回收,想象一下如果A是个比较复杂的对象或者集合类型会发生什么情况。
Android中常见的内存泄漏汇总
1.集合类泄露
集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性,全局性的map等即有静态引用或final一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。比如上面的典型例子就是其中一种情况,当然实际上我们在项目中肯定不会写这样的代码,但是稍不注意还是很容易出现这种情况,比如我们都喜欢通过HashMap做一些缓存之类的事情,这种情况一定要注意。
2.单例造成的内存泄漏
由于单例的静态特性使得其生命周期跟应用的生命周期一样长,如果使用不恰当,很容易造成内存泄漏,比如下面一个例子:
public class AppManager{ private static AppManager instanse; private Context context; private AppManager(Context context){ this.context = context; } public static AppManager getInstance(Context context){ if(instance == null){ instance = new AppManager(context); } return instance; } }
这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
- 如果此时传入的是Application的Context,因为Application的生命周期就是整个应用的生命周期,所以这将没有任何问题。
- 如果此时传入的是Activity的Context,当这个Context所对应的Activity退出时,由于该Context的引用被单例对象所持有,整个生命周期等于整个应用的生命周期,所以当前Activity退出时它的内存不会被回收,这就造成了泄漏了。
正确的方式应该改为下面这种方式:
public class AppManager{ private static AppManager instanse; private Context context; private AppManager(Context context){ this.context = context.getApplicationContext();//使用Application的context } public static AppManager getInstance(Context context){ if(instance == null){ instance = new AppManager(context); } return instance; } }
或者这样写,连Context都不用传进来了:
在你的Application里面添加一个静态方法,getContext()返回Application的context ... context = getApplicationContext(); ... /**获取全局的Context*/ public static Context getContext(){ return context; } public class AppManager{ private static AppManager instanse; private Context context; private AppManager(){ this.context = AppManager.getContext(); } public static AppManager getInstance(Context context){ if(instance == null){ instance = new AppManager(context); } return instance; } }
3.匿名内部类/非静态内部类和异步线程
非静态内部类创建静态实例造成的内存泄漏
有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:
public class MainActivity extends AppCompatActivity{ private static TestResource mResource = null; @Override protected void onCreate(Bundle saveInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(mResource==null){ mResource = new TestResource(); } //... } class TestResource{ //... } }
这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。
正确的做法为:
将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application的Context。当然,Application的context也不是万能的,所以也不能随便乱用,对于有些地方则必须使用Activity的Context(如start an Activity、show a dialog)。
其实Application和Service可以启动一个Activity,不过需要创建一个新的task任务队列,而对于Dialog而言,只有在Activity中才能创建。
4.匿名内部类
android开发经常会继承实现Activity/Fragment/View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄漏。
public class MainActivity extends Activity{ ... Runnable ref1 = new MyRunnable(); Runnable ref2 = new MyRunnable(){ @Override public void run(){ ... } }; ... }
ref1和ref2的区别是,ref2使用了匿名内部类。此时ref2这个匿名类的实现对象里面多了一个引用,也就是说当前的MainActivity实例会被ref2持有,如果将这个引用再传入一个异步线程,此线程和此Activity生命周期不一致的时候,就造成了Activity的泄露。
5.Handler造成的内存泄漏
Handler的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免ANR而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,但Handler不是万能的,对于Handler的使用代码编写一不规范即有可能造成内存泄漏。另外,我们知道Handler、Message和MessageQueue都是相互关联在一起的,万一Handler发送的Message尚未被处理,则该Message及发送它的Handler对象将被线程MessageQueue一直持有。
public class SampleActivity extends Activity { private final Handler mLeakyHandler = new Handler() { @Override public void handleMessage(Message msg) { // ... } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Post a message and delay its execution for 10 minutes. mLeakyHandler.postDelayed(new Runnable() { @Override public void run() { /* ... */ } }, 1000 * 60 * 10); // Go back to the previous Activity. finish(); } }
在该SampleActivity中声明了一个延迟10分钟执行的消息Message,mLeakyHandler将其push进了消息队列MessageQueue里,当该Activity被finish掉时,延迟执行任务的Message还会继续存在主线程中,它持有该Activity的Handler引用,所以此时finish掉的Activity就不会被回收了从而造成内存泄漏(因Handler为静态内部类,它会持有外部类的引用,在这里就是指SampleActivity)。
修复方法:在Activity中避免使用非静态内部类,比如上面我们将Handler声明为静态的,其存活期跟Activity的生命周期就无关了,同时通过弱引用的方式引入Activity,避免直接将Activity作为context传进去。
public class SampleActivity extends Activity { /** * Instances of static inner classes do not hold an implicit * reference to their outer class. */ private static class MyHandler extends Handler { private final WeakReference mActivity; public MyHandler(SampleActivity activity) { mActivity = new WeakReference(activity); } @Override public void handleMessage(Message msg) { SampleActivity activity = mActivity.get(); if (activity != null) { // ... } } } private final MyHandler mHandler = new MyHandler(this); /** * Instances of anonymous classes do not hold an implicit * reference to their outer class when they are "static". */ private static final Runnable sRunnable = new Runnable() { @Override public void run() { /* ... */ } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Post a message and delay its execution for 10 minutes. mHandler.postDelayed(sRunnable, 1000 * 60 * 10); // Go back to the previous Activity. finish(); } }
综述,即推荐使用静态内部类+WeakReference这种方式。每次使用前注意判空。
前面提到了WeakReference,所以这里就简单的说一下Java对象的几种引用类型。
Java对引用的分类有StrongReference,SoftReference,WeakReference,PhatomReference四种。
在Android应用开发中,为了防止内存溢出,在处理一些占用内存大而且周期较长的对象时候,可以尽量应用软引用和弱引用技术。
软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清楚已失效的软/弱引用。
假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到,如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取出来,但是,由于图片占用的内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OOM,这是我们可以考虑使用软/弱引用计数来避免这个问题的发生,以下就是高速缓冲器的雏形:
首先定义一个HashMap保存软引用对象。
public class CacheBySoftRef {
//首先定义一个HashMap,保存软引用对象
private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
//再来定义一个方法,保存Bitmap的软引用到HashMap
public void addBitmapToCache(String path) {
//强引用的Bitmap对象
Bitmap bitmap = BitmapFactory.decodeFile(path);
//软引用的Bitmap对象
SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
//添加该对象到Map中使其缓存
imageCache.put(path, softBitmap);
}
//获取的时候,可以通过SoftReference的get()方法得到Bitmap对象
public Bitmap getBitmapByPath(String path) {
//从缓存中取软引用的Bitmap对象
SoftReference<Bitmap> softBitmap = imageCache.get(path);
//判断是否存在软引用
if (softBitmap == null) {
return null;
}
//通过软引用取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空,如果未被回收则可重复使用,提高速度。
Bitmap bitmap = softBitmap.get();
return bitmap;
}
}
使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。
如果只是想避免OutOifMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
另外可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量使用软引用,如果该对象不被使用的可能性更大一些,就可以使用弱引用。
回到主题,前面所说的创建一个静态的Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,但是这样做虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destory时或者Stop时应该移除消息队列MessageQueue中的消息。
6.尽量避免使用static成员变量
如果成员变量被声明为static,那我们都知道其生命周期将与整个app进程生命周期一样。
这会导致一系列问题,如果你的App进程设计上是常驻内存的,那即是app切换到后台,这部分内存也不会释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,因为如果此app做过进程互相保活,那么会造成app在后台频繁重启。
7.避免override finalize()
8.资源为关闭造成的内存泄漏
对于使用了BroadcastReceiver,ContentObserver,File,游标Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时即是关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
9.一些不良代码造成的内存压力
比如,Bitmap没有调用recycle()方法,对于Bitmap对象在不使用时候,我们应该先调用recycle()释放内存,然后将它设置为null,因为家在Bitmap对象的内存空间,一部分是Java的,一部分是C的(因为Bitmap分配的底层是通过JNI调用的)。而这个recycle()就是针对C部分的内存释放。构造Adapter时,没有使用缓存的convertView,每次都在创建新的convertView,这里一定要做listView的优化。
总结
对Activity等组件的引用应该控制在Activity的生命周期内;如果不能就考虑使用getApplicationContext或者getApplication,以避免Activity被外部长生命周期的对象引用而泄漏。
尽量不要在静态变量或者静态内部类中使用非静态内部成员变量(包括context),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。
对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:
将内部类改为静态内部类、静态内部类中使用弱引用来引用外部类的成员变量。
Handler所持有的引用对象最好也是用弱引用,资源释放时也可以清空Handler里面的消息。
在Java的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用array.clear();array=null)等,最好遵循谁创建谁释放的原则。
正确关闭资源,对于使用了BroadcastReceiver、contentObserve、File、游标Cursor、Stream、Bitmap等资源,应该在Activity销毁时及时关闭或者注销。
保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。