最近項目中頻繁出現(xiàn)OOM的問題,各種路徑測試、內(nèi)存走向分析、各種邏輯推理才最終定位到問題。在這過程中和組內(nèi)的同學討論的時候發(fā)現(xiàn)有的同學對內(nèi)存泄漏和內(nèi)存溢出的概念理解不到位,導致溝通過程比較尷尬。很多同學對這兩個概念理解不夠透徹,在項目中頻繁寫出內(nèi)存泄漏的低級代碼出來。結合自己的理解我寫一篇文章理解下這兩個概念。
內(nèi)存泄漏
內(nèi)存泄漏是指那些本應該回收(不再使用)的內(nèi)存對象無法被系統(tǒng)回收的現(xiàn)象。在c++中需要程序猿手動釋放內(nèi)存對象,所以在C++中更容易存在內(nèi)存泄漏。java引入了自動回收機制,使得在C++中令人頭疼的內(nèi)存問題得到了有效的改善,但這并不意味著java程序員不關注內(nèi)存,因為垃圾回收機制不能完全保證內(nèi)存對象在該釋放的地方釋放,現(xiàn)代java虛擬機中普遍使用根集算法去計算對象的引用可達性,不可達的才能回收,例如下圖中的無用對象被有用對象引用著,導致無用對象引用一直可達,系統(tǒng)回收器不敢冒然回收,從而造成內(nèi)存泄漏。

內(nèi)存溢出
系統(tǒng)在為某段執(zhí)行指令(程序)分配內(nèi)存的時候,發(fā)現(xiàn)內(nèi)存不足,拋出錯誤,這叫做內(nèi)存溢出。

二者關系
手機設備的內(nèi)存空間是有限的,為每個應用所分配到的內(nèi)存空間更是有限的,當內(nèi)存泄漏對象越來越多,可調(diào)配內(nèi)存空間就越小,App應用性能越差,當可調(diào)配的內(nèi)存空間不夠創(chuàng)建新對象時就會引起OOM。

內(nèi)存泄漏經(jīng)典模型
靜態(tài)變量
靜態(tài)變量的生命周期是最長的,和應用程序的生命周期一樣,當一個大對象被一個類的靜態(tài)變量引用時,這個對象就無法被系統(tǒng)回收,在應用的整個生命周期中占用內(nèi)存。常見于工具類,一般中存在大量的工具類,很多同學圖方便直接或間接使用靜態(tài)變量引用一個上下文對象的。
public class NotificationUtil {
//靜態(tài)變量,notificationManager泄漏
private static NotificationManager notificationManager;
public static void notification(Context context, Class<?> cls, Message msg) {
if (notificationManager == null){
notificationManager = (NotificationManager)
context.getSystemService(context.NOTIFICATION_SERVICE);
}
//....
}
}
NotificationManager 對象泄漏了,同時因為 NotificationManager 對象中有一個上下文對象mContext變量,又回引起啟動這個方法的Context對象泄漏。
規(guī)避:
對于工具類,如非頻繁使用的對象,盡量不要使用 static 變量去引用,可以在方法執(zhí)行時候再創(chuàng)建,作為局部變量使用;如需要頻繁使用,為了提高方法執(zhí)行效率,對于上面這種情況可以把Context 參數(shù)限制為Application 級別的上下文,避免調(diào)用方傳遞Activity級別的上下文,造成Activity泄漏。
public class NotificationUtil {
//靜態(tài)變量
private static NotificationManager notificationManager;
public static void notification(Application context, Class<?> cls, Message msg) {
//context限制為Application級別
if (notificationManager == null){
notificationManager = (NotificationManager)
context.getSystemService(context.NOTIFICATION_SERVICE);
}
//.....
}
}
單例模式
單例模式其實也是靜態(tài)變量的一種,單例的生命周期和靜態(tài)變量時一樣的,如果這個單例中持有一個大對象,就會引起這個大對象泄漏。
private static WebViewClient instance;
public static WebViewClient getInstance(Context context) {
if (instance == null) {
//mContext有泄漏風險
instance = new WebViewClient(context, jsToJava);
}
return instance;
}
例如這里的mContext,如果是個Activity的話,會被instance長期引用著的。
規(guī)避:
和靜態(tài)變量一樣的道理,盡量使用Application級別的上下文代替Activity級別的上下文。
private static WebViewClient instance;
public static WebViewClient getInstance(Application context) {
//context限制為Application級別的
if (instance == null) {
instance = new WebViewClient(context, jsToJava);
}
return instance;
}
內(nèi)部類
由于內(nèi)部類的存在需要依賴它的外部類,由于某些原因?qū)е聝?nèi)部類被引用會無法退出,引起外部類無法回收,這是使用最多也是最容易被用出內(nèi)存泄漏的了。

內(nèi)部類循環(huán)或者阻塞,下面就有一個奇葩的代碼,一個研究生寫的:
import android.content.Context;
import android.util.AttributeSet;
import android.widget.EditText;
public class CheckEditText extends EditText{
public CheckEditText(Context context,AttributeSet attrs){
super(context,attrs);
new Thread(){
@Override
public void run(){
while(true) {
String content = this.getText().toString();
if(content.length() > 12){
//字符長度不能大于12
//...
}
}
}
}.start();
}
}
然后所有使用這個CheckEditText的Activity都泄漏了。

這種模式最常見的就是Handler的使用了,一般項目中很多內(nèi)存泄漏就是這種模型:
//內(nèi)部類的Handler,有內(nèi)存泄漏風險
Handler handler = new Handler() {
public void handleMessage(android.os.Message msg) {
int what = msg.what;
switch (what) {
case SHOW:
progressDialog = ProgressDialog.show(DetailActivity.this, null, "圖片上傳中...");
break;
case DISMISS:
if (progressDialog != null && progressDialog.isShowing()) {
progressDialog.dismiss();
}
break;
case UPLOADPIC:
String base64ImageString = (String) msg.obj;
webView.loadUrl("javascript:fileChooserCallback(" + "'" + base64ImageString
+ "'" + ")");
break;
default:
break;
}
}
};
這是DetailActivity中的一個Handler內(nèi)部類,這會導致DetailActivity泄漏,因為Handler其實是被一個消息隊列引用著。
規(guī)避:
對于那些可能長時間執(zhí)行、阻塞或者被外部引用的內(nèi)部盡量使用靜態(tài)內(nèi)部類代替。靜態(tài)內(nèi)部類對象的存在不依附外部類,這樣可以避開內(nèi)部類對外部類的隱性引用,然后使用弱引用持有外部類對象。
static class MyHandler extends Handler {
//靜態(tài)內(nèi)部類代替,并使用若引用持有DetailActivity
private WeakReference<DetailActivity> weakReference;
public MyHandler(DetailActivity activity) {
this.weakReference = new WeakReference(activity);
}
public void handleMessage(android.os.Message msg) {
int what = msg.what;
DetailActivity activity = weakReference.get();
if (activity == null){
return;
}
switch (what) {
case SHOW:
activity.progressDialog = ProgressDialog.show(activity, null, "圖片上傳中...");
break;
case DISMISS:
if (activity.progressDialog != null && activity.progressDialog.isShowing()) {
activity.progressDialog.dismiss();
}
break;
case UPLOADPIC:
String base64ImageString = (String) msg.obj;
activity.webView.loadUrl("javascript:fileChooserCallback(" + "'" + base64ImageString
+ "'" + ")");
break;
default:
break;
}
}
};
MyHandler handler = new MyHandler(this);