Skip to content

Latest commit

 

History

History
259 lines (228 loc) · 11 KB

内存泄漏.md

File metadata and controls

259 lines (228 loc) · 11 KB

内存泄露

定义    

内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。

原因      

长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收, 这就是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.

JavaC++一个很大的区别就是Java有垃圾回收GC(Garbage Collection)自动管理内存的回收。但是我们在实际的项目中仍然会遇到内存泄露的问题。 Java中对内存对象得访问是通过引用的方式,通过一个内存对象的引用变量来访问到对应的内存地址中的对象。 GC会从代码栈的引用变量开始追踪,从而判断哪些内存是正在使用,如果无法跟踪到某一块堆内存,那么GC就认为这块内存不再使用了。

Android手机给应用分配的内存通常是8兆左右,如果处理内存处理不当很容易造成OutOfMemoryError OutOfMemoryError主要由以下几种情况造成:

  1. 数据库Cursor没关。
    当我们操作完数据库后,一定要调用close()释放资源。

  2. 构造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;  
    }  
  3. 未取消注册广播接收者
    registerReceiver()unregisterReceiver()要成对出现,通常需要在ActivityonDestory()方法去取消注册广播接收者。

  4. IO流未关闭 注意用完后及时关闭

  5. Bitmap使用后未调用recycle()

  6. 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类型)的引用, 因为sBackgroundstatic的,即使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之后已经被修复。

  7. 线程 线程也是造成内存泄露的一个重要的源头。线程产生内存泄露的主要原因在于线程生命周期的不可控。我们来考虑下面一段代码。

    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();             
    		//耗时的操作       
    		}     
    	} 
    }  

    假设MyThreadrun函数是一个很费时的操作,当调用finish的时候Activity会销毁掉吗?
    事实上由于我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThreadrun函数没有结束时,MyThread是不会被销毁的, 因此它所引用的Activity也不会被销毁,因此就出现了内存泄露的问题。

  8. 单例造成的内存泄漏
    由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏,比如:

    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;
    	}
    }

    这里如果传入的是ActivityContext,当该ContextActivity退出后,由于其被单例对象引用,所以会导致Activity无法被回收,就造成内存泄漏。

  9. 尽量使用ApplicationContext Context引用的生命周期超过它本身的生命周期,也会导致Context泄漏。 所以如果打算保存一个长时间的对象时尽量使用Application这种Context类型。 例如:

    mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
    改成mStorageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);

    按道理来说这种系统服务是不会有问题,但是有些厂商在修改的时候,可能会导致Context无法被及时释放。

  10. 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);  
    } 
  11. 由上面的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的不一致时就会造成了泄漏。

  12. 集合类泄漏
    集合类中如果只有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性),那么没有 响应的删除机制,很可能导致集合所占用的内存只增不减。

总结一下避免Context泄漏应该注意的问题:

  • 使用getApplicationContext()类型。
  • 注意对Context的引用不要超过它本身的生命周期。
  • 慎重的使用static关键字。
  • Activity里如果有线程或Handler时,一定要在onDestroy()里及时停掉。