什么是内存泄漏
对于java而言,就是存放在堆上的Object无法被GC正常回收。
产生的根本原因:
长生命周期的对象持有短生命周期的强/软引用,导致本应被回收的短生命周期对象无法被正常回收。
例如在单例模式中,我们常常在获取单例对象时需要传一个 Context 。单例对象是一个长生命周期的对象(应用程序结束时才终结),而如果我们传递的是某一个 Activity 作为 context,那么这个 Activity 就会因为引用被持有而无法销毁,从而导致内存泄漏。
造成影响:
- 应用可能内存减少,增加了堆内存压力
- 降低应用的性能,比如会触发更频繁的GC(垃圾回收)
- 严重时会导致OOM Error
内存泄露对安卓应用造成的危害:
- 运行性能的问题: Android在运行的时候,如果内存泄漏将导致其他组件可用的内存变少,一方面会使得GC的频率加剧,在发生GC的时候,所有进程都必须进行等待,GC的频率越多,从而用户越容易感知到卡顿。另一方面,内存变少,将可能使得系统会额外分配给你一些内存,而影响整个系统的运行状况。
- 运行崩溃问题: 内存泄露是内存溢出(OOM)的重要原因之一,会导致 Crash。如果应用程序在消耗光了所有的可用堆空间,那么再试图在堆上分配新对象时就会引起 OOM(Out Of Memory Error) 异常,此时应用程序就会崩溃退出。
java内存分配基础:
- 方法区:编译时分配好,在程序整个运行期间都存在。主要存放静态数据和常量。
- 栈区:方法执行时,存放局部变量,方法结束后自动释放内存。
- 堆区:通常用来存放new 出来的对象。由GC进行回收。
java四种不同引用类型
- 强引用(Strong Reference):JVM 宁愿抛出 OOM,也不会让 GC 回收存在强引用的对象。
- 软引用(Soft Reference):一个对象只具有软引用,在内存不足时,这个对象才会被 GC 回收。
- 弱引用(weak Reference):在 GC 时,如果一个对象只存在弱引用,那么它将会被回收。
- 虚引用(Phantom Reference):任何时候都可以被 GC 回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为 GC 回收 Object 的标志。
与Android的差异:
在 2.3 以后版本中,即使内存够用,Android 系统会优先将 SoftReference 的对象提前回收掉, 其他和 Java 中是一样的。
因此谷歌官方建议用LruCache(least recentlly use 最少最近使用算法)。会将内存控制在一定的大小内, 超出最大值时会自动回收, 这个最大值开发者自己定。
两个概念
- GC( Garbage Collection)垃圾回收。
方法 System.gc
- LruCache(least Recently Used),最近最少使用。
LRU算法就是当缓存空间满了的时候,将最近最少使用的数据从缓存空间中删除以增加可用的缓存空间来缓存新数据。这个算法的内部有一个缓存列表,每当一个缓存数据被访问的时候,这个数据就会被提到列表尾部,每次都这样的话,列表的头部数据就是最近最不常使用的了,当缓存空间不足时,就会删除列表头部的缓存数据。
示例代码:
//获取系统分配给每个应用程序的最大内存
int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
int cacheSize=maxMemory/8;
private LruCache<String, Bitmap> mMemoryCache;
//给LruCache分配1/8
mMemoryCache = new LruCache<String, Bitmap>(mCacheSize){
//重写该方法,来测量Bitmap的大小
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight()/1024;
}
};
内存泄露案例:
1. 单例模式产生的内存泄漏
由于单例模式的静态特性,使得它的生命周期和我们的应用一样长,一不小心让单例无限制的持有 Activity 的强引用就会导致内存泄漏。
解决方法:
把传入的Context改为同应用生命周期一样长的Application的Context,即getApplicationContext。
2. Handler引发的内存泄漏
由于 Handler 属于 TLS(Thread Local Storage)变量,导致它的生命周期和 Activity 不一致。因此通过 Handler 来更新 UI 一般很难保证跟 View 或者 Activity 的生命周期一致,故很容易导致无法正确释放。
public class HandlerBadActivity extends AppCompatActivity {
private final Handler handler = new Handler(){//非静态内部类,持有外部类的强引用
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_bad);
// 延迟 5min 发送一个消息
handler.postDelayed(new Runnable() {
//内部会将该 Runable 封装为一个 Message 对象,同时将 Message.target 赋值为 handler
@Override
public void run() {
//do something
}
}, 1000 * 60 * 5);
this.finish();
}
}
上面的代码中发送了了一个延时 5 分钟执行的 Message,当该 Activity 退出的时候,延时任务(Message)还在主线程的 MessageQueue 中等待,此时的 Message 持有 Handler 的强引用(创建时通过 Message.target 进行指定),并且由于 Handler 是 HandlerBadActivity 的非静态内部类,所以 Handler 会持有一个指向 HandlerBadActivity 的强引用,所以虽然此时 HandlerBadActivity 调用了 finish 也无法进行内存回收,造成内存泄漏。
怎么解决?
将 Handler 声明为静态内部类,但是要注意如果用到 Context 等外部类的 非static 对象,还是应该使用 ApplicationContext 或者通过弱引用来持有这些外部对象。
public class HandlerGoodActivity extends AppCompatActivity {
private static final class MyHandler extends Handler{//声明为静态内部类(避免持有外部类的强引用)
private final WeakReference<HandlerGoodActivity> mActivity;
public MyHandler(HandlerGoodActivity activity){
this.mActivity = new WeakReference<HandlerGoodActivity>(activity);//使用弱引用
}
@Override
public void handleMessage(Message msg) {
HandlerGoodActivity activity = mActivity.get();
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {//判断 activity 是否为空,以及是否正在被销毁、或者已经被销毁
removeCallbacksAndMessages(null);
return;
}
// do something
}
}
private final MyHandler myHandler = new MyHandler(this);
}
3. 慎用static成员变量
static 修饰的变量位于内存的方法区,其生命周期与 App 的生命周期一致。这必然会导致一些问题。
Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于Application,Service,Activity三者的Context的应用场景如下:
使用场景
4. 集合引发的内存泄漏
集合在使用完毕,退出页面前,通过remove方法,或者clear置空,并赋值null。
Android应用性能优化文章参考:https://www.jianshu.com/p/754f7607c869
网友评论