定义
内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。
原因
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,
这就是java
中内存泄露的发生场景。
危害
只有一个,那就是虚拟机占用内存过高,导致OOM
(内存溢出)。
对于Android
应用来说,就是你的用户打开一个Activity
,使用完之后关闭它,内存泄露;又打开,又关闭,又泄露;几次之后,程序占用内存超过系统限制就会出现FC
。
先来说一下Java
程序运行时的内存分配策略:
- 静态内存:存放静态数据,这块内存是在编译时就已经分配好的,在程序整个运行期间都存在。
- 栈内存:程序执行时,局部变量的创建存储区,执行结束后将自动释放。(当时学习java时花的内存分配图,现在都生疏了- -!)
- 堆内存:存储一些new出来的内存,也叫动态内存分配,这部分内存在不使用时将会有
Java
垃圾回收器来负责回收。
举一个典型的内存泄漏的例子:
Vector v = new Vector(100);
for (int i = 1; i < 100,; i++) {
Object o = new Object();
v.add(o);
o = null;
}
在这个例子中,我们循环申请了Object
对象,并将所申请的对象放入一个集合中,如果我们仅仅释放引用本身,那么Vector
仍然引用
该对象,所以这个对象对GC
来说是不可回收的。因此,如果对象假如Vector
后,还必须从Vector
中删除,最简单的方法就是将
Vector
对象设置为null
.
Java
和C++
一个很大的区别就是Java
有垃圾回收GC(Garbage Collection)
自动管理内存的回收。但是我们在实际的项目中仍然会遇到内存泄露的问题。
Java
中对内存对象得访问是通过引用的方式,通过一个内存对象的引用变量来访问到对应的内存地址中的对象。
GC
会从代码栈的引用变量开始追踪,从而判断哪些内存是正在使用,如果无法跟踪到某一块堆内存,那么GC
就认为这块内存不再使用了。
Android
手机给应用分配的内存通常是8兆左右,如果处理内存处理不当很容易造成OutOfMemoryError
OutOfMemoryError
主要由以下几种情况造成:
- 数据库
Cursor
没关。
当我们操作完数据库后,一定要调用close()
释放资源。 -
构造
Adapter
没有使用缓存ContentView
。@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder vHolder = null; //如果convertView对象为空则创建新对象,不为空则复用 if (convertView == null) { convertView = inflater.inflate(..., null); // 创建 ViewHodler 对象 vHolder = new ViewHolder(); vHolder.img= (ImageView) convertView.findViewById(...); vHolder.tv= (TextView) convertView .findViewById(...); // 将ViewHodler保存到Tag中 convertView.setTag(vHolder); } else { //当convertView不为空时,通过getTag()得到View vHolder = (ViewHolder) convertView.getTag(); } // 给对象赋值,修改显示的值 vHolder.img.setImageBitmap(...); vHolder.tv.setText(...); return convertView; } static class ViewHolder { TextView tv; ImageView img; }
- 未取消注册广播接收者
registerReceiver()
和unregisterReceiver()
要成对出现,通常需要在Activity
的onDestory()
方法去取消注册广播接收者。 -
IO
流未关闭
注意用完后及时关闭 -
Bitmap
使用后未调用recycle()
。 -
Context
泄漏
这是一个很隐晦的OutOfMemoryError
的情况。先看一个Android官网提供的例子:private static Drawable sBackground; @Override protected void onCreate(Bundle state) { super.onCreate(state); TextView label = new TextView(this); label.setText("Leaks are bad"); if (sBackground == null) { sBackground = getDrawable(R.drawable.large_bitmap); } label.setBackgroundDrawable(sBackground); setContentView(label); }
这段代码效率很快,但同时又是极其错误的:
我们看一下setBackgroundDrawable(Drawable background)
的源码:public void setBackgroundDrawable(Drawable background) { ... background.setCallback(this); }
有
background.setCallback(this);
方法,也就是说Drawable
拥有TextView
的引用,而TextView
又拥有Activity
(Context类型)的引用,
因为sBackground
为static
的,即使Activity
被销毁,但是sBackground
的生命周期还没走完,所以内存仍然不会被释放。这样就会有内存泄露了。
对,这样想是对的,但是我们看一下setCallback
的源码:public final void setCallback(Callback cb) { mCallback = new WeakReference<Callback>(cb); }
我们会发现里面使用了
WeakReference
,所以不会存在内存泄露了,但是官网当时提供的例子明明说有泄露,这是因为在3.0之后,
修复了这个内存泄露的问题,在3.0之前setCallback
,方法是没有使用WeakReference
的,所以这种泄露的情况在3.0之前会发生,3.0之后已经被修复。 -
线程
线程也是造成内存泄露的一个重要的源头。线程产生内存泄露的主要原因在于线程生命周期的不可控。我们来考虑下面一段代码。public class MyActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); new MyThread().start(); } private class MyThread extends Thread{ @Override public void run() { super.run(); //耗时的操作 } } }
假设
MyThread
的run
函数是一个很费时的操作,当调用finish
的时候Activity
会销毁掉吗?
事实上由于我们的线程是Activity
的内部类,所以MyThread
中保存了Activity
的一个引用,当MyThread
的run
函数没有结束时,MyThread
是不会被销毁的,
因此它所引用的Activity
也不会被销毁,因此就出现了内存泄露的问题。 -
单例造成的内存泄漏
由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏,比如:public class AppManager { private static AppManager instance; 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; } }
这里如果传入的是
Activity
的Context
,当该Context
的Activity
退出后,由于其被单例对象引用,所以会导致Activity
无法被回收,就造成内存泄漏。 -
尽量使用
ApplicationContext
Context
引用的生命周期超过它本身的生命周期,也会导致Context
泄漏。
所以如果打算保存一个长时间的对象时尽量使用Application
这种Context
类型。
例如:mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE); 改成: mStorageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);
按道理来说这种系统服务是不会有问题,但是有些厂商在修改的时候,可能会导致
Context
无法被及时释放。 -
Handler的使用,在Activity退出的时候注意移除(尤其是循环的时候)
public class ThreadDemo extends Activity { private static final String TAG = "ThreadDemo"; private int count = 0; private Handler mHandler = new Handler(); private Runnable mRunnable = new Runnable() { public void run() { //为了方便 查看,我们用Log打印出来 Log.e(TAG, Thread.currentThread().getName() + " " +count); //每2秒执行一次 mHandler.postDelayed(mRunnable, 2000); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //通过Handler启动线程 mHandler.post(mRunnable); } }
这样在也会引发内存泄露。我们应该在
onDestory
方法中移除Handler
,代码如下:@Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacks(mRunnable); }
- 由上面的
Handler
可以引伸出来的匿名内部类、非静态内部类和异步现成导致的内存泄漏。
下面看一个非静态内部类创建静态实例导致的内存泄漏public class MainActivity extends AppCompatActivity { private static TestResource mResource = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(mManager == null){ mManager = new TestResource(); } //... } class TestResource { //... } }
因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态实例,该实例的声明周期与应用的一样长,
这就导致了静态实例一直会持有Activity
的引用而造成内存泄漏。
下面再看一个匿名内部类和异步现成的现象:public class MainActivity extends Activity { ... Runnable ref1 = new MyRunable(); Runnable ref2 = new Runnable() { @Override public void run() { } }; ... }
上面的例子中
ref1
对象是没问题的,但是ref2
这个匿名类的实现对象中有外部类的引用,如果此时线程的生命周期与Activity
的不一致时就会造成了泄漏。 -
集合类泄漏
集合类中如果只有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性),那么没有
响应的删除机制,很可能导致集合所占用的内存只增不减。
总结一下避免Contex
t泄漏应该注意的问题:
- 使用
getApplicationContext()
类型。 - 注意对
Context
的引用不要超过它本身的生命周期。 - 慎重的使用
static
关键字。 Activity
里如果有线程或Handler
时,一定要在onDestroy()
里及时停掉。