基礎知識
Java 的記憶體分配簡述
- 方法區(non-heap):編譯時就分配好,在程式整個執行期間都存在。它主要存放靜態資料和常量;
- 棧區:當方法執行時,會在棧區記憶體中建立方法體內部的區域性變數,方法結束後自動釋放記憶體;
- 堆區(heap):通常用來存放 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 最少最近使用演算法)。會將記憶體控制在一定的大小內, 超出最大值時會自動回收, 這個最大值開發者自己定。
什麼是記憶體洩漏?
- 對於 C++ 來說,記憶體洩漏就是 new 出來的物件沒有 delete,俗稱野指標;
- 而對於 java 而言,就是存放在堆上的 Object 無法被 GC 正常回收。
記憶體洩漏根本原因
長生命週期的物件
持有短生命週期物件**強/軟引用**
,導致本應該被回收的短生命週期的物件卻無法被正常回收。
例如在單例模式中,我們常常在獲取單例物件時需要傳一個 Context 。單例物件是一個長生命週期的物件(應用程式結束時才終結),而如果我們傳遞的是某一個 Activity 作為 context,那麼這個 Activity 就會因為引用被持有而無法銷燬,從而導致記憶體洩漏。
記憶體洩漏的危害
- 執行效能的問題: Android在執行的時候,如果記憶體洩漏將導致其他元件可用的記憶體變少,一方面會使得GC的頻率加劇,在發生GC的時候,所有程式都必須進行等待,GC的頻率越多,從而使用者越容易感知到卡頓。另一方面,記憶體變少,將可能使得系統會額外分配給你一些記憶體,而影響整個系統的執行狀況。
- 執行崩潰問題: 記憶體洩露是記憶體溢位(OOM)的重要原因之一,會導致 Crash。如果應用程式在消耗光了所有的可用堆空間,那麼再試圖在堆上分配新物件時就會引起 OOM(Out Of Memory Error) 異常,此時應用程式就會崩潰退出。
記憶體洩漏的典型案例
永遠的單例(Singleton)
由於單例模式的靜態特性,使得它的生命週期和我們的應用一樣長,一不小心讓單例無限制的持有 Activity 的強引用就會導致記憶體洩漏。
解決方案
- 把傳入的 Context 改為同應用生命週期一樣長的 Application 中的 Context。
- 通過重寫 Application,提供 getContext 方法,那樣就不需要在獲取單例時傳入 context。
public class BaseApplication extends Application{
private static ApplicationContext sContext;
@Override
public void onCreate(){
super.onCreate();
sContext = getApplicationContext();
}
public static Context getApplicationContext(){
return sContext;
}
}
複製程式碼
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);
}
複製程式碼
慎用 static 成員變數
static 修飾的變數位於記憶體的方法區,其生命週期與 App 的生命週期一致
。
這必然會導致一系列問題,如果你的 app 程式設計上是長駐記憶體的,那即使 app 切到後臺,這部分記憶體也不會被釋放。
解決方法
不要在類初始化時初始化靜態成員,也就是可以考慮懶載入。架構設計上要思考是否真的有必要這樣做,儘量避免。如果架構需要這麼設計,那麼此物件的生命週期你有責任管理起來。
當然,Application 的 context 不是萬能的,所以也不能隨便亂用,對於有些地方則必須使用 Activity 的 Context,對於Application,Service,Activity三者的Context的應用場景如下:
功能 | Application | Service | Activity |
---|---|---|---|
Start an Activity | NO1 | NO1 | YES |
Show a Dialog | NO | NO | YES |
Layout Inflation | NO1 | NO1 | YES |
Start an Service | NO1 | NO1 | YES |
Bind an Service | NO1 | NO1 | YES |
Send a Broadcast | NO1 | NO1 | YES |
Register BroadcastReceiver | NO1 | NO1 | YES |
Load Resource Values | NO1 | NO1 | YES |
- NO1 表示 Application 和 Service 可以啟動一個 Activity,不過
需要建立一個新的 task 任務佇列
。- 對於 Dialog 而言,只有在 Activity 中才能建立。
使用系統服務引發的記憶體洩漏
為了方便我們使用一些常見的系統服務,Activity 做了一些封裝。比如說,可以通過 getPackageManager
在 Activtiy 中獲取 PackageManagerService
,但是,裡面實際上呼叫了 Activity 對應的 ContextImpl 中的 getPackageManager 方法
ContextWrapper#getPackageManager
@Override
public PackageManager getPackageManager() {
return mBase.getPackageManager();
}
複製程式碼
ContextImpl#getPackageManager
@Override
public PackageManager getPackageManager() {
if (mPackageManager != null) {
return mPackageManager;
}
IPackageManager pm = ActivityThread.getPackageManager();
if (pm != null) {
// Doesn't matter if we make more than one instance.
return (mPackageManager = new ApplicationPackageManager(this, pm));//建立 ApplicationPackageManager
}
return null;
}
複製程式碼
ApplicationPackageManager#ApplicationPackageManager
ApplicationPackageManager(ContextImpl context,
IPackageManager pm) {
mContext = context;//儲存 ContextImpl 的強引用
mPM = pm;
}
private UserManagerService(Context context, PackageManagerService pm,
Object packagesLock, File dataDir) {
mContext = context;//持有外部 Context 引用
mPm = pm;
//程式碼省略
}
複製程式碼
PackageManagerService#PackageManagerService
public class PackageManagerService extends IPackageManager.Stub {
static UserManagerService sUserManager;//持有 UMS 靜態引用
public PackageManagerService(Context context, Installer installer,
boolean factoryTest, boolean onlyCore) {
sUserManager = new UserManagerService(context, this, mPackages);//初始化 UMS
}
}
複製程式碼
遇到的記憶體洩漏問題是因為在 Activity 中呼叫了 getPackageManger 方法獲取 PMS ,該方法呼叫的是 ContextImpl,此時如果ContextImpl 中 PackageManager 為 null,就會建立一個 PackageManger(ContextImpl 會將自己傳遞進去,而 ContextImpl 的 mOuterContext 為 Activity),建立 PackageManager 實際上會建立 PackageManagerService(簡稱 PMS),而 PMS 的構造方法中會建立一個 UserManger(UserManger 初始化之後會持有 ContextImpl 的強引用)。
只要 PMS 的 class 未被銷燬,那麼就會一直引用著 UserManger ,進而導致其關聯到的資源無法正常釋放。
解決辦法
將getPackageManager()
改為getApplication()#getPackageManager()
。這樣引用的就是 Application Context,而非 Activity 了。
遠離非靜態內部類和匿名類
因為使用非靜態內部類和匿名類都會預設持有外部類的引用,如果生命週期不一致,就會導致記憶體洩漏。
public class NestedClassLeakActivity extends AppCompatActivity {
class InnerClass {//非靜態內部類
}
private static InnerClass sInner;//指向非靜態內部類的靜態引用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nested_class);
if (sInner == null) {
sInner = new InnerClass();//建立非靜態內部類的例項
}
}
}
複製程式碼
非靜態內部類預設會持有外部類的引用,而外部類中又有一個該非靜態內部類的靜態例項,該靜態例項的生命週期和應用的一樣長,而靜態例項又持有 Activity 的引用,因此導致 Activity 的記憶體資源不能正常回收。
解決方法
將該內部類設為靜態內部類 也可以將該內部類抽取出來封裝成一個單例
集合引發的記憶體洩漏
我們通常會把一些物件的引用加入到集合容器(比如ArrayList)中,當我們不再需要該物件時(通常會呼叫 remove 方法),並沒有把它的引用從集合中清理掉(其中的一種情況就是 remove 方法沒有將不再需要的引用賦值為 null),下面以 ArrayList 的 remove 方法為例
public E remove( int index) {
// 陣列越界檢查
RangeCheck(index);
modCount++;
// 取出要刪除位置的元素,供返回使用
E oldValue = (E) elementData[index];
// 計算陣列要複製的數量
int numMoved = size - index - 1;
// 陣列複製,就是將index之後的元素往前移動一個位置
if (numMoved > 0)
System. arraycopy(elementData, index+1, elementData, index,
numMoved);
// 將陣列最後一個元素置空(因為刪除了一個元素,然後index後面的元素都向前移動了,所以最後一個就沒用了),好讓gc儘快回收
elementData[--size ] = null; // Let gc do its work
return oldValue;
}
複製程式碼
WebView 引發的記憶體洩漏
WebView 解析網頁時會申請Native堆記憶體
用於儲存頁面元素,當頁面較複雜時會有很大的記憶體佔用。如果頁面包含圖片,記憶體佔用會更嚴重。並且開啟新頁面時,為了能快速回退,之前頁面佔用的記憶體也不會釋放
。有時瀏覽十幾個網頁,都會佔用幾百兆的記憶體。這樣載入網頁較多時,會導致系統不堪重負,最終強制關閉應用,也就是出現應用閃退或重啟。
由於佔用的都是Native 堆記憶體
,所以實際佔用的記憶體大小不會顯示在常用的 DDMS Heap 工具中
( DMS Heap 工具看到的只是Java虛擬機器分配的記憶體,即使Native堆記憶體已經佔用了幾百兆,這裡顯示的還只是幾兆或十幾兆)。只有使用 adb shell 中的一些命令比如 dumpsys meminfo 包名,或者在程式中使用 Debug.getNativeHeapSize()
才能看到 Native 堆記憶體資訊。
據說由於 WebView 的一個 BUG,即使它所在的 Activity(或者Service) 結束也就是 onDestroy() 之後,或者直接呼叫 WebView.destroy()之後,它所佔用這些記憶體也不會被釋放。
解決方法
把使用了 WebView 的 Activity (或者 Service) 放在單獨的程式裡。
- 系統在檢測到應用佔用記憶體過大有可能被系統幹掉
- 也可以在它所在的 Activity(或者 Service) 結束後,呼叫 System.exit(0),主動Kill掉程式。由於系統的記憶體分配是以程式為準的,程式關閉後,系統會自動回收所有記憶體。
使用 WebView 的頁面(Activity),在生命週期結束頁面退出(onDestory)的時候,主動呼叫WebView.onPause()==以及==WebView.destory()以便讓系統釋放 WebView 相關資源。
其他常見的引起記憶體洩漏原因
- Android 3.0 以下,Bitmap 在不使用的時候沒有使用 recycle() 釋放記憶體。
非靜態內部類的靜態例項
容易造成記憶體洩漏:即一個類中如果你不能夠控制它其中內部類的生命週期(譬如Activity中的一些特殊Handler等),則儘量使用靜態類和弱引用來處理(譬如ViewRoot的實現)。警惕執行緒未終止造成的記憶體洩露
;譬如在 Activity 中關聯了一個生命週期超過 Activity 的 Thread,在退出 Activity 時切記結束執行緒。一個典型的例子就是 HandlerThread 的 run 方法。該方法在這裡是一個死迴圈,它不會自己結束,執行緒的生命週期超過了 Activity 生命週期,我們必須手動在 Activity 的銷燬方法中中呼叫 thread.getLooper().quit() 才不會洩露。
物件的註冊與反註冊沒有成對出現
造成的記憶體洩露;譬如註冊廣播接收器、註冊觀察者(典型的譬如資料庫的監聽)等。建立與關閉沒有成對出現造成的洩露
;譬如Cursor資源必須手動關閉,WebView必須手動銷燬,流等物件必須手動關閉等。- 避免程式碼設計模式的錯誤造成記憶體洩露;譬如迴圈引用,A 持有 B,B 持有 C,C 持有 A,這樣的設計誰都得不到釋放。