在Android開發中,內存泄漏是比較常見的,大多數開發者都知道有這麼一回事,但是不清楚是什麼原因會導致內存泄漏。在此就分享下自己的看法。
在講內存泄漏之前,我們先了解下Java虛擬機內存模型和GC算法,這樣我們能更好的理解後面說的幾種情況爲什麼會導致內存泄漏。
Java虛擬機內存模型
在Java虛擬機規範中指明瞭Java虛擬機運行時的內存模型,如下圖所示
- Java虛擬機棧:虛擬機棧描述的是Java方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀( Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。
- 本地方法棧:和Java虛擬機棧類似,只不過是用於Native方法。
- 程序計數器:可以理解成當前線程所執行的字節碼行號指示器,用於控制字節碼的執行順序。
- 堆:Java堆是虛擬機內存最大的一塊區域,用於存放對象實例,它是被所有線程共享的內存區域。GC管理的主要也是Java堆,也就是說GC回收的內存主要是堆內存
- 方法區:方法區是被各個線程共享的內存區域,該區域用於存放被虛擬機加載的類信息,常量,靜態變量等數據
GC算法
我們知道Android4.4及以前採用的是Dalvik虛擬機,Android5.0以後採用的是ART替代Dalvik虛擬機,雖然Dalvik和ART不是Java虛擬機,但是他們的內存模型是相似的。我們以Dalvik虛擬機爲例講解下它的GC算法。
Dalvik
Android的Dalvik虛擬機的GC算法採用的是Mark-Sweep(標記-清除)算法,所謂標記-清除算法,從字面理解可知該算法有兩個階段:標記階段和清除階段。標記階段是把所有活動對象都做上標記的階段;清除階段是把那些沒有標記的對象,也就是非活動對象回收的階段。具體的算法實現我們先不關注,主要關注如何區分“活動對象”和“非活動對象”。在Dalvik虛擬機中判斷對象是否是活動對象的方式是判斷該對象和“GC Roots對象”是否存在引用鏈。
如上圖所示和GC Roots存在存在引用鏈的Obj1、Obj2、Obj3、Obj4就屬於活動對象;Obj5、Obj6、Obj7因爲和GC Roots對象不存在引用關係,它們就屬於非活動對象,而這些對象佔用的堆內存在清除階段就會被回收。現在就有個關鍵點,哪些對象可以是“GC Roots對象”呢,GC Roots對象包括以下幾種:
- 方法區中的常量引用的對象;
- 方法區中靜態變量引用的對象;
- 本地方法棧中JNI引用的對象;
- 虛擬機棧中引用的對象;
內存泄漏
內存泄漏是指程序中己動態分配的堆內存由於某種原因程序未釋放或無法釋放。在Android中常見的內存泄漏場景有:
- 靜態變量導致的內存泄漏;
- 匿名內部類和非靜態內部類導致的內存泄漏;
- 集合類導致的內存泄漏;
- 資源未關閉導致的內存泄漏;
靜態變量導致的內存泄漏
前面我們知道,靜態變量引用的對象屬於“GC Roots對象”,所以當我們定義靜態變量對象的時候就要留意該對象本身和與該對象存在引用關係的對象是否會導致內存泄漏。下面有幾個與靜態變量相關的例子:
單例
靜態變量導致的內存泄漏中有個常見的例子,就是在單例中需要到Context對象的時候,很多人可能會這樣寫:
public class SingleInstanceLeak {
private static final String TAG = "SingleInstanceLeak";
private Context mContext;
private static SingleInstanceLeak sInstance;
private SingleInstanceLeak (Context context) {
mContext = context;
}
public static SingleInstanceLeak getInstance (Context context) {
if (sInstance == null) {
sInstance = new SingleInstanceLeak(context);
}
return sInstance;
}
public void testLog(String msg) {
Log.d(TAG, "testLog, msg: " + msg);
Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
}
}
使用時可能是在Activity中調用靜態方法SingleInstanceLeak.getInstance(this)得到SingleInstanceLeak對象,然後調用SingleInstanceLeak中的方法。當靜態變量sInstance實例化後,該對象就持有了Context對象的引用,這就會導致內存泄漏。在前面我們知道靜態變量引用的對象是“GC Roots對象”,這樣就導致如果sInstance對象沒有手動釋放的話,Context對象在發生GC並不會被回收的,因爲虛擬機認爲它是活動的對象。
避免內存泄漏的單例有多種,可以使用WeakReference引用,也可以使用Application Context,個人比較推薦以下這種寫法,在自己的Application提供靜態方法返回Application對象的引用供外部使用:
public class SingleInstanceNoLeak {
private static final String TAG = "SingleInstanceLeak";
private Context mContext;
private static SingleInstanceNoLeak sInstance;
private SingleInstanceNoLeak() {
mContext = MyApplication.getInstance();
}
public static SingleInstanceNoLeak getInstance() {
if (sInstance == null) {
sInstance = new SingleInstanceNoLeak();
}
return sInstance;
}
public void testLog(String msg) {
Log.d(TAG, "testLog, msg: " + msg);
Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
}
}
匿名內部類和非靜態內部類導致的內存泄漏
匿名內部類和非靜態內部類可能會導致內存泄漏的原因是他們會持有外部類的引用。
在Android中匿名內部類導致的內存泄漏有個很常見的情況,就是在使用Handler的時候,很多人一般都是這樣寫的:
public class AnonymousInnerActivity extends AppCompatActivity {
private static final int MSG_UPDATE_TEXT = 101;
@BindView(R.id.tv_anonymous_text)
TextView mTvAnonymous;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anonymous_inner);
ButterKnife.bind(this);
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_UPDATE_TEXT:
mTvAnonymous.setText("Anonymous");
break;
}
}
};
@OnClick(R.id.btn_anonymous)
void onViewClicked(View v) {
switch (v.getId()) {
case R.id.btn_anonymous:
mHandler.sendEmptyMessageDelayed(MSG_UPDATE_TEXT, 30 * 1000);
finish();
break;
}
}
}
這種寫法會導致內存泄漏,因爲如果Message沒有被處理的話,Handler會一直持有Activity引用,導致GC無法回收Activity對象,因爲這個時候Activity對象和GC Roots對象之間是可達的,以下是他們之間的引用關係概要圖和時序圖。
從上面的引用關係鏈圖中我們知道Activity對象和GC Roots對象是存在引用關係的,這就導致GC時,Activity對象不會被回收,造成內存泄漏。通過以下的時序圖能更清楚地瞭解調用的過程,從而跟蹤他們之間的引用關係。
改善的寫法,可以採用靜態內部類和弱引用的方式來替代上面的方法,避免內存泄漏,具體如下:
public class AnonymousInnerActivity extends AppCompatActivity {
private static final int MSG_UPDATE_TEXT = 101;
@BindView(R.id.tv_anonymous_text)
TextView mTvAnonymous;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anonymous_inner);
ButterKnife.bind(this);
mHandler = new MyHandler(this);
}
@OnClick(R.id.btn_anonymous)
void onViewClicked(View v) {
switch (v.getId()) {
case R.id.btn_anonymous:
mHandler.sendEmptyMessageDelayed(MSG_UPDATE_TEXT, 30 * 1000);
finish();
break;
}
}
private static class MyHandler extends Handler {
private WeakReference<AnonymousInnerActivity> mActivity;
private MyHandler(AnonymousInnerActivity reference) {
mActivity = new WeakReference<>(reference);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_UPDATE_TEXT:
if (mActivity.get() != null) {
mActivity.get().mTvAnonymous.setText("Anonymous");
}
break;
}
}
}
}
在靜態內部類MyHanler中持有的是AnonymousInnerActivity的弱引用,當發生GC時,GC會回收弱引用所指向的對象;但匿名內部類中持有的是強引用,GC並不會回收強引用所指向的對象。這也就是爲什麼採用靜態內部類和弱引用能避免內存泄漏,而匿名內部類會導致內存泄漏的原因。 內部類導致內存泄漏的原因也類似,就不舉例了。
集合類導致的內存泄漏
集合導致內存泄漏是指集合只有添加對象,沒有刪除對象,導致集合佔有的內存只有增不減,從而導致內存泄漏。這種情況不常見,但是不注意還是可能出現,比如下面的例子
public class InfoCache {
private static List<Info> sInfoList = new ArrayList<>(20);
public static void addInfo(Info info) {
sInfoList.add(info);
}
public static List<Info> getInfoList() {
return sInfoList;
}
public static class Info {
private byte[] mBytes = new byte[1024 * 1024];
}
}
一個通過集合緩存的例子,如果外部只調用InfoCache.addInfo()向集合中添加元素,而沒有清除集合中的元素時,就可能會導致內存泄漏。雖然這種寫法很少發生,但是還是需要注意。
資源未關閉導致的內存泄漏
在Android中由於資源沒有關閉導致的內存泄漏主要是在使用BroadcastReceiver、ContentObserver、Cursor、I/O流、Bitmap等沒有及時的反註冊和關閉,導致這些資源不能被回收,造成內存泄漏。改善方法沒啥好說的, 只要在編碼中注意BroadcastReceiver、ContentObserver要在反註冊,Cursor和I/O流記得關閉;在用到ImageView顯示圖片的時候,如果圖片原始尺寸比ImageView要顯示的尺寸大,記得采用BitmapFactory.Option進行縮放,在不需要顯示的時候調用Bitmap.recycle()方法釋放。
##總結
要理解上面說的幾種情況爲什麼會導致內存泄漏,我們先要明白GC判斷對象是否存活的算法,它是通過判斷對象和GC Roots對象間是否可達,從而決定哪些對象會被回收,哪些不會被回收。既然知道了對象存活是跟GC Roots 對象有關,那麼如果我們知道哪些對象可以作爲GC Roots對象就知道哪些情況會導致內存泄漏了。