[TOC]
# 內存泄漏
本文對網絡上的內存泄漏相關知識進行分析、學習,并對一些知識點提出了自己的見解,包括什么是內存泄漏、可能出現內存泄漏的情況以及如何避免內存泄漏。
## 什么是內存泄漏
存在下面的這種對象,這些對象不會被 GC 回收,卻占用著內存,即為內存泄漏(簡單說:存在已申請的無用內存無法被回收)
* 該對象是可達的,即還在被引用著
* 該對象是無用的,即程序以后不會再使用該對象
## 可能出現內存泄漏的情況
長生命周期的對象持有短生命周期的引用,就很可能會出現內存泄露。

對于網上內存泄漏情況分析的文章,我個人持有一些不同的觀點,下面按情況來分析:
### 靜態集合未及時刪除無用對象
網上比較多的文章在這個例子下都是拿 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 之外:

## 總結
其實,歸結于一句話,避免內存泄漏,主要還是:
避免長生命周期對象持有短生命周期對象的引用。
參考文檔:
[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零碎問題
- 其他零碎問題
- 開發思路