ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] # 内存泄漏 本文对网络上的内存泄漏相关知识进行分析、学习,并对一些知识点提出了自己的见解,包括什么是内存泄漏、可能出现内存泄漏的情况以及如何避免内存泄漏。 ## 什么是内存泄漏 存在下面的这种对象,这些对象不会被 GC 回收,却占用着内存,即为内存泄漏(简单说:存在已申请的无用内存无法被回收) * 该对象是可达的,即还在被引用着 * 该对象是无用的,即程序以后不会再使用该对象 ## 可能出现内存泄漏的情况 长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。 ![](https://img.kancloud.cn/6d/2c/6d2c2189caf6773382c1642b5b3a4280_3056x1296.png) 对于网上内存泄漏情况分析的文章,我个人持有一些不同的观点,下面按情况来分析: ### 静态集合未及时删除无用对象 网上比较多的文章在这个例子下都是拿 Vector 作为例子,Vector 其实是一个已经过时、废弃的类了,下面以最常见的 ArrayList 来分析: ```java static List<Object> sObjectList = new ArrayList<>(); for (int i = 0; i < 100; i++) { Object o = new Object(); sObjectList.add(o); } ``` 当某些对象无用,我们需要将其手动从集合中移除时,此时会删除集合对对象的引用,避免内存泄漏发送。这一点我们可以从 JDK 的 ArrayList 源码中看到: ```java public E remove(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); modCount++; E oldValue = (E) elementData[index]; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; } ``` 可以看到在倒数第三行将集合内部对象引用置为 null,此时该对象内存可得到回收。如果不及时将无用对象从集合中移除,由于静态类的生命周期和应用的周期一样长,所有集合元素对象会一直保持强引用状态,易造成内存泄漏。 ### HashSet存储对象的属性被修改,调用HashSet的remove方法失效 小提示:实现 Set 接口的集合元素为无序、不可重复的 网上对这种类型的内存泄漏描述为: > 当集合里面的对象属性被修改后,再调用 remove() 方法时不起作用 我认为应该描述为 HashSet 才准确,因为其他集合并不存在这种问题。要了解在 HashSet 中为什么不起作用,我们需要先看看 HashSet 的实现方式。HashSet 的存储实现其实是依赖于 HashMap,在调用 HashSet 的 add(Object o)方法时,实际上是将对象 o 作为 key,虚拟了一个对象作为 value,将这个 Entry 存入 HashSet 所持有的一个 HashMap 中。源码如下: ```java // ... // HashSet 维护的 HashMap private transient HashMap<E,Object> map; // 与 HashMap Entry相关连的虚拟的值 private static final Object PRESENT = new Object(); public boolean add(E e) { return map.put(e, PRESENT)==null; } // ... ``` 所以就可以理解为什么网上都是以下面代码作为这种内存泄漏的例子了。 ```java HashSet<Student> hashSet = new HashSet<>(); Student xiaoming = new Student("xiaoming", 15); Student xiaogang = new Student("xiaogang", 14); hashSet.add(xiaoming); hashSet.add(xiaogang); xiaoming.setAge(16); // 此时 xiaoming 对象的 hashCode 值发生改变 hashSet.remove(xiaoming); hashSet.add(xiaoming); ``` 对照代码看: * 第 6 行:由于更改了对象的属性之后,其 hashCode 会变化 * 第 7 行:从 HashSet 中移除元素时其实就是从 HashSet 持有的那个 HashMap 中移除,在从 HashMap 中根据 key 移除条目时,会计算 key 的 hashCode,而这个 key 其实就是 HashSet 中的元素对象 xiaoming,它的 hashCode 已经改变了 * 第 8 行:所以会移除失败,并且可以再次添加 xiaoming 对象,因为它的 hashCode 变了 然后 HashMap 根据计算得到的 hash 值,再从存储结构中寻找指定的条目并移除,具体实现就不在本文研究范围之内了。 ### 非静态内部类持有外部类引用 且 非静态内部类对象生命周期大于外部类对象生命周期 #### 监听器 关于监听器导致的内存泄漏,网上描述如下: > 在 java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如 addXXXListener() 等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。 其实描述并无问题,但没说明为什么不删除监听器就会导致内存泄漏。 其实不光监听器,Java 所有的非静态内部类都有可能造成内存泄漏,当然监听器属于匿名内部类,也算非静态内部类的一种。 非静态内部类包括成员内部类、局部内部类和匿名内部类。在编译器进行编译时,会自动为非静态内部类添加一个指向外部类的成员变量,所以非静态内部类持有外部类的引用。下面举两个 Android 开发中常见的例子: #### 多线程 ```java new Thread(new Runnable() { @Override public void run() { try { // 模拟耗时操作 Thread.sleep(30000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); ``` 在外部类退出时,耗时操作仍未完成的情况下,子线程仍会持有外部类的引用,导致外部类占用空间无法回收,造成内存泄漏。 #### Handler ```java public class MainActivity extends AppCompatActivity { private Handler mHandler = new MyHandler(); // 此处 MyHandler 为成员内部类 class MyHandler extends Handler { @Override public void handleMessage(Message msg) { super.handleMessage(msg); // do something } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mHandler.postDelayed(new Runnable() { @Override public void run() { // do something } }, 1000 * 60 * 10); } } ``` 上面例子中,Handler为成员内部类,持有外部类Activity对象的引用。Activity退出后,Handler消息延迟10分钟发出,Activity引用无法释放,造成内存泄漏。解决方案为:a、将Handler改为静态内部类;b、在Activity退出时清空消息队列。 ### 连接未及时释放 及时关闭各种数据库连接、Socket连接、IO连接等 ### 单例模式使用生命周期短的Context ```java public class Singleton { private static volatile Singleton sInstance; private Context mContext; public static synchronized Singleton getInstance(Context context) { if (sInstance == null) { synchronized (Singleton.class) { if (sInstance == null) { sInstance = new Singleton(context); } } } return sInstance; } private Singleton(Context context) { mContext = context; } } ``` 在上面这段代码中可以看到,单例模式的 getInstance 方法需要一个 Context 参数,这个参数很关键。因为单例对象的生命周期和应用的生命周期一样长,如果在首次调用 getInstance 方法时传入的 Context 是 Activity,那么单例对象会持有该 Activity 的引用,当该 Activity 生命周期结束时,Activity 依然被生命周期更长的单例对象引用,GC 无法回收其内存导致内存泄漏。 ## 避免内存泄漏的方法 ### 将非静态内部类改为静态内部类 2017 年 10 月 21 日更新:下面这种写法是有问题的,会造成内存泄漏。 ```java private static class MyRunnable implements Runnable { WeakReference<MainActivity> mWeakReference; public MyRunnable(MainActivity mainActivity) { mWeakReference = new WeakReference<>(mainActivity); } @Override public void run() { MainActivity mainActivity = mWeakReference.get(); // get 方法会获得强引用 if (mainActivity != null) { // 模拟耗时操作 try { Thread.sleep(30000); } catch (InterruptedException e) { e.printStackTrace(); } mainActivity.mStudent.setAge(12); } } } ``` 此时 MyRunnable 成为静态内部类,不再持有外部类的引用。但同时出现一个问题,MyRunnable 内部无法使用外部类的非静态成员变量了,此时可以让 MyRunnable 类持有外部类的弱引用,通过外部类的弱引用调用其非静态成员变量。 最近项目中的代码改成这样子的写法,但是在实际测试过程中发现,依旧会造成内存泄漏。分析结果如下: 当在调用 new Thread(new MyRunnable(MainActivity.this)).start 时,MyRunnable 持有了 MainActivity 的引用,为弱引用。但是在执行到 run 方法时,mainActivity 通过 mWeakReference.get 方法获取到了 MainActivity 的实例对象,为强引用。如果在线程耗时操作过程中退出 MainActivity,此时子线程依旧会持有 MainActivity 的强引用会造成内存泄漏。 对于这种情况,我暂时采取了添加标识位,在 Activity 退出时,结束子线程。[结束子线程的方法参考链接](http://www.jianshu.com/p/536b0df1fd55) 而对于 Handler,则可以使用上述解决方案,如下: ```java private static class MyHandler extends Handler { private WeakReference<MainActivity> mWeakReference; MyHandler(MainActivity MainActivity) { mWeakReference = new WeakReference<>(MainActivity); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); MainActivity activity = mWeakReference.get(); if (activity != null) { switch (msg.what) { case FINISH_SWIPE_CARD: activity.mScanBar.setVisibility(View.GONE); break; default: break; } } } } ``` ### 通过程序逻辑切段引用 由于内部类持有外部类的引用导致内存泄漏,那么我们就把这条路切断,就可以避免内存泄漏。 * 在外部类退出的时候,结束掉后台子线程,引用就被切断 * 如果是 handler 时,要判断到底是谁持有了 handler 引用。如果是后台线程,那么就结束掉后台线程;如果是 delay 的 message 持有,那么可以清除掉消息队列所有消息,切断 Looper 消息队列对 handler 的引用: ```java @Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); } ``` ### 单例模式使用生命周期更长的 Context 在单例模式引起的内存泄漏中,为了避免长生命周期的单例引用短生命周期的 Activity,我们应当使用生命周期更长的 ApplicationContext: ```java public class Singleton { private static volatile Singleton sInstance; private Context mContext; public static synchronized Singleton getInstance(Context context) { if (sInstance == null) { synchronized (Singleton.class) { if (sInstance == null) { sInstance = new Singleton(context); } } } return sInstance; } private Singleton(Context context) { mContext = context.getApplicationContext(); } } ``` 我们在项目中也应当优先使用 ApplicationContext,它可以适用于大部分情况,除了 startActivity 和 show dialog 之外: ![](https://ws1.sinaimg.cn/large/006tKfTcgy1fkuqqx5y4xj30qi09et8u.jpg) ## 总结 其实,归结于一句话,避免内存泄漏,主要还是: 避免长生命周期对象持有短生命周期对象的引用。 参考文档: [Android 中使用 Handler 造成内存泄露的分析和解决-扔物线](https://my.oschina.net/rengwuxian/blog/181449) [Android 中 Handler 引起的内存泄露-技术小黑屋](http://droidyue.com/blog/2014/12/28/in-android-handler-classes-should-be-static-or-leaks-might-occur/index.html) [android-内部类导致的内存泄漏实战解析](http://blog.csdn.net/sinat_31057219/article/details/74533647) [内存泄露浅析](http://afayp.me/2016/09/23/%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E6%B5%85%E6%9E%90/)