[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/)
- 导读
- Java知识
- Java基本程序设计结构
- 【基础知识】Java基础
- 【源码分析】Okio
- 【源码分析】深入理解i++和++i
- 【专题分析】JVM与GC
- 【面试清单】Java基本程序设计结构
- 对象与类
- 【基础知识】对象与类
- 【专题分析】Java类加载过程
- 【面试清单】对象与类
- 泛型
- 【基础知识】泛型
- 【面试清单】泛型
- 集合
- 【基础知识】集合
- 【源码分析】SparseArray
- 【面试清单】集合
- 多线程
- 【基础知识】多线程
- 【源码分析】ThreadPoolExecutor源码分析
- 【专题分析】volatile关键字
- 【面试清单】多线程
- Java新特性
- 【专题分析】Lambda表达式
- 【专题分析】注解
- 【面试清单】Java新特性
- Effective Java笔记
- Android知识
- Activity
- 【基础知识】Activity
- 【专题分析】运行时权限
- 【专题分析】使用Intent打开三方应用
- 【源码分析】Activity的工作过程
- 【面试清单】Activity
- 架构组件
- 【专题分析】MVC、MVP与MVVM
- 【专题分析】数据绑定
- 【面试清单】架构组件
- 界面
- 【专题分析】自定义View
- 【专题分析】ImageView的ScaleType属性
- 【专题分析】ConstraintLayout 使用
- 【专题分析】搞懂点九图
- 【专题分析】Adapter
- 【源码分析】LayoutInflater
- 【源码分析】ViewStub
- 【源码分析】View三大流程
- 【源码分析】触摸事件分发机制
- 【源码分析】按键事件分发机制
- 【源码分析】Android窗口机制
- 【面试清单】界面
- 动画和过渡
- 【基础知识】动画和过渡
- 【面试清单】动画和过渡
- 图片和图形
- 【专题分析】图片加载
- 【面试清单】图片和图形
- 后台任务
- 应用数据和文件
- 基于网络的内容
- 多线程与多进程
- 【基础知识】多线程与多进程
- 【源码分析】Handler
- 【源码分析】AsyncTask
- 【专题分析】Service
- 【源码分析】Parcelable
- 【专题分析】Binder
- 【源码分析】Messenger
- 【面试清单】多线程与多进程
- 应用优化
- 【专题分析】布局优化
- 【专题分析】绘制优化
- 【专题分析】内存优化
- 【专题分析】启动优化
- 【专题分析】电池优化
- 【专题分析】包大小优化
- 【面试清单】应用优化
- Android新特性
- 【专题分析】状态栏、ActionBar和导航栏
- 【专题分析】应用图标、通知栏适配
- 【专题分析】Android新版本重要变更
- 【专题分析】唯一标识符的最佳做法
- 开源库源码分析
- 【源码分析】BaseRecyclerViewAdapterHelper
- 【源码分析】ButterKnife
- 【源码分析】Dagger2
- 【源码分析】EventBus3(一)
- 【源码分析】EventBus3(二)
- 【源码分析】Glide
- 【源码分析】OkHttp
- 【源码分析】Retrofit
- 其他知识
- Flutter
- 原生开发与跨平台开发
- 整体归纳
- 状态及状态管理
- 零碎知识点
- 添加Flutter到现有应用
- Git知识
- Git命令
- .gitignore文件
- 设计模式
- 创建型模式
- 结构型模式
- 行为型模式
- RxJava
- 基础
- Linux知识
- 环境变量
- Linux命令
- ADB命令
- 算法
- 常见数据结构及实现
- 数组
- 排序算法
- 链表
- 二叉树
- 栈和队列
- 算法时间复杂度
- 常见算法思想
- 其他技术
- 正则表达式
- 编码格式
- HTTP与HTTPS
- 【面试清单】其他知识
- 开发归纳
- Android零碎问题
- 其他零碎问题
- 开发思路