內存泄漏與內存溢出總結

導讀:

本篇文章是最近幾天關於內存優化的個人學習總結,從基礎到日常常見的內存泄漏的順序慢慢介紹…本編全文本,可能有些單調,不過認真看下來,肯定收益良多!

如果急着解決,直接看 “常見的內存溢出處理”,”常見的內存泄漏”

java 內存分配策略

Java 程序運行時的內存分配策略有三種,分別是靜態分配,棧式分配,和堆式分配,對應的,三種存儲策略使用的內存空間主要分別是靜態存儲區(也稱方法區)、棧區和堆區。

  • 靜態存儲區(方法區):主要存放靜態數據、全局 static 數據和常量。這塊內存在程序編譯時就已經分配好,並且在程序整個運行期間都存在。

  • 棧區 :當方法被執行時,方法體內的局部變量(其中包括基礎數據類型、對象的引用)都在棧上創建,並在方法執行結束時這些局部變量所持有的內存將會自動被釋放。因爲棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限。

  • 堆區 : 又稱動態內存分配,通常就是指在程序運行時直接 new 出來的內存,也就是對象的實例。這部分內存在不使用時將會由 Java 垃圾回收器來負責回收。

棧與堆的區別:

在方法體內定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內存中分配的。

當在一段方法塊中定義一個變量時,Java就會在棧中爲該變量分配內存空間,當超過該變量的作用域後,該變量也就無效了,分配給它的內存空間也將被釋放掉,該內存空間可以被重新使用。

堆內存用來存放所有由 new 創建的對象(包括該對象其中的所有成員變量)和數組。

在堆中分配的內存,將由Java垃圾回收器來自動管理。在堆中產生了一個數組或者對象後,還可以在棧中定義一個特殊的變量,這個變量的取值等於數組或者對象在堆內存中的首地址,這個特殊的變量就是我們上面說的引用變量。我們可以通過這個引用變量來訪問堆中的對象或者數組。

例子:

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();
    }
}

Sample mSample3 = new Sample();

Sample 類的局部變量 s2 和引用變量 mSample2 都是存在於棧中,但 mSample2 指向的對象是存在於堆上的。 mSample3 指向的對象實體存放在堆上,包括這個對象的所有成員變量 s1 和 mSample1,而它自己存在於棧中。

結論:

局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲於堆中。—— 因爲它們屬於方法中的變量,生命週期隨方法而結束。

成員變量全部存儲與堆中(包括基本數據類型,引用和引用的對象實體)—— 因爲它們屬於類,類對象終究是要被new出來使用的。

java中的四種引用類型:

  • 強引用(StrongReference):JVM 寧可拋出 OOM ,也不會讓 GC 回收具有強引用的對象;

如:User u=new User();
User u 存在於棧裏面,new User() 存在於堆裏面的,棧通過 = 號,將堆對象引用起來,叫強引用(當前這種形式稱爲顯式的強引用(強可及對象))

  • 軟引用(SoftReference):只有在內存空間不足時,纔會被回的對象;

  • 弱引用(WeakReference):在 GC 時,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存;

  • 虛引用(PhantomReference):任何時候都可以被GC回收,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收。可以用來作爲GC回收Object的標誌。

軟引用和弱引用,這兩個引用是可以隨時被虛擬機回收的對象,我們將一些比較佔內存但是又可能後面用的對象,比如Bitmap對象,可以聲明爲軟引用或弱引用。但是注意一點,每次使用這個對象時候,需要顯示判斷一下是否爲null,以免出錯。

什麼是垃圾回收機制?

JVM的垃圾回收機制中,判斷一個對象是否死亡,並不是根據是否還有對象對其有引用,而是通過可達性分析。對象之間的引用可以抽象成樹形結構,通過樹根(GC Roots)作爲起點,從這些樹根往下搜索,搜索走過的鏈稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明這個對象是不可用的,該對象會被判定爲可回收的對象。

那麼哪些對象可作爲GC Roots(GC 會自動回收的對象)呢?主要有以下幾種:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  2. 方法區中類靜態屬性引用的對象。
  3. 方法區中常量引用的對象
  4. 本地方法棧中JNI(即一般說的Native方法)引用的對象。
  5. Thread —活着的線程

一般出問題時也是不巧當的調用以上對象造成的

內存泄漏與內存溢出的區別:

內存泄漏 memory leak

  • 指程序申請了內存後(new),用完的內存沒有釋放(delete),一直被某個或某些實例所持有卻不再被使用導致 GC 不能回收
  • 生活例子 : 電熱水器洗完澡不關水,其他人用就沒熱水的情況
  • 內存泄漏是導致內存溢出的原因之一;內存泄漏累積起來就會造成內存溢出
  • 內存泄漏可以通過完善代碼來避免

內存溢出 out of memory

  • 指程序申請內存時,沒有足夠的內存空間使用
  • 生活例子 : 水杯滿了還往裏面加水
  • 內存溢出可以通過調整配置來減少發生頻率,無法徹底避免

常見內存溢出處理:

絕大部分的內存溢出原因是由於圖片太大引起的
一般我們使用軟引用/弱引用解決由於圖片資源過大的內存溢出
但是API9以後,GC機制改變了,建議使用LruCache緩衝圖片資源
注意臨時Bitmap對象的及時回收,先recycle(),後致空
儘量避免Try catch某些大杯存分配的操作
加載Bitmap時:縮放比例、解碼格式、局部加載(圖片大於手機屏幕)

圖片處理(防止內存溢出) 看這篇

內存泄漏的原因:

以發生的方式來分類,內存泄漏可以分爲4類:

  1. 常發性內存泄漏 : 發生內存泄漏的代碼會被多次執行到,每次被執行的時候都會導致一塊內存泄漏。

  2. 偶發性內存泄漏 : 發生內存泄漏的代碼只有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測內存泄漏至關重要。

  3. 一次性內存泄漏 : 發生內存泄漏的代碼只會被執行一次,或者由於算法上的缺陷,導致總會有一塊僅且一塊內存發生泄漏。比如,在類的構造函數中分配內存,在析構函數中卻沒有釋放該內存,所以內存泄漏只會發生一次。

  4. 隱式內存泄漏 : 程序在運行過程中不停的分配內存,但是直到結束的時候才釋放內存。嚴格的說這裏並沒有發生內存泄漏,因爲最終程序釋放了所有申請的內存。但是對於一個服務器程序,需要運行幾天,幾周甚至幾個月,不及時釋放內存也可能導致最終耗盡系統的所有內存。所以,我們稱這類內存泄漏爲隱式內存泄漏。(重視)

小結:

一次性內存泄漏沒有什麼危害,因爲它不會堆積,而隱式內存泄漏危害性則非常大,因爲較於常發性和偶發性內存泄漏它更難被檢測到

個人總結的內存泄漏:

單例模式造成的內存泄漏

由於單例的靜態特性使得單例的生命週期和應用的生命週期一樣長,這就說明了如果一個對象已經不需要使用了,而單例對象還持有該對象的引用,那麼這個對象將不能被正常回收,這就導致了內存泄漏。

案例:
public class AppManager {      

private static AppManager instance;     
private Context context;    

private AppManager(Context context) {        
this.context = context;   
}      

public static AppManager getInstance(Context context) {       

if (instance != null) {              

instance = new AppManager(context);  


}          

return instance;   

} 
}

這是一個普通的單例模式,當創建這個單例的時候,由於需要傳入一個Context,所以這個Context的生命週期的長短至關重要: 

1、傳入的是Application的Context:這將沒有任何問題,因爲單例的生命週期和Application的一樣長 ;

2、傳入的是Activity的Context:當這個Context所對應的Activity退出時,由於該Context和Activity的生命週期一樣長(Activity間接繼承於Context),所以當前Activity退出時它的內存並不會被回收,因爲單例對象持有該Activity的引用。(static > Activty 生命週期) 

所以正確的單例應該修改爲下面這種方式:

public class AppManager { 

    private static AppManager instance;    
     private Context context;      

    private AppManager(Context context) {         

    this.context = context.getApplicationContext();     
    }      

    public static AppManager getInstance(Context context) {        
    if (instance != null) {              

    instance = new AppManager(context);       
    }      
    return instance;    

    } 

}

這樣不管傳入什麼Context最終將使用Application的Context,而單例的生命週期和應用的一樣長,這樣就防止了內存泄漏。


第二種方式:這樣寫,連Context都不用傳進來了:

在你的 Application 中添加一個靜態方法,getContext() 返回 Application 的 context,

...

context = getApplicationContext();

...
   /**
     * 獲取全局的context
     * @return 返回全局context對象
     */
    public static Context getContext(){
        return context;
    }

public class AppManager {
private static AppManager instance;
private Context context;
private AppManager() {
this.context = MyApplication.getContext();// 使用Application 的context
}
public static AppManager getInstance() {
if (instance == null) {
instance = new AppManager();
}
return instance;
}
}

非靜態內部類創建靜態實例造成的內存泄漏

public class MainActivity extends AppCompatActivity {    
    private static TestResource mResource = null; 

    @Override      
    protected void onCreate(Bundle savedInstanceState) {       
         super.onCreate(savedInstanceState);        
    setContentView(R.layout.activity_main);       

    if(mManager == null){         
    mManager = new TestResource();

     }       

     //...   
     }     

     class TestResource {      
     //...    
     }


} 
這樣就在Activity內部創建了一個非靜態內部類的單例,每次啓動Activity時都會使用該單例的數據
,這樣雖然避免了資源的重複創建,不過這種寫法卻會造成內存泄漏
,因爲非靜態內部類默認會持有外部類的引用,而又使用了該非靜態內部類創建了一個靜態的實例,
該實例的生命週期和應用的一樣長,這就導致了該靜態實例一直會持有該Activity的引用,導致Activity的內存資源不能正常回收。

正確的做法爲: 

將該內部類設爲靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,請使用ApplicationContext 。

Handler造成的內存泄漏

Handler的使用造成的內存泄漏問題應該說最爲常見了,平時在處理網絡任務或者封裝一些請求回調等api都應該會藉助Handler來處理,對於Handler的使用代碼編寫不規範即有可能造成內存泄漏

如下示例:

public class MainActivity extends AppCompatActivity {    
private Handler mHandler = new Handler() {       

@Override          
public void handleMessage(Message msg) {           
//...       

}    


};     
@Override      
protected void onCreate(Bundle savedInstanceState) {         
super.onCreate(savedInstanceState);         
setContentView(R.layout.activity_main);         
loadData();   
}


private void loadData(){       
//...request        
Message message = Message.obtain();       
mHandler.sendMessage(message); 
    }
    }  

    這種創建Handler的方式會造成內存泄漏,由於mHandler是Handler的非靜態匿名內部類的實例,所以它持有外部類Activity的引用,我們知道消息隊列是在一個Looper線程中不斷輪詢處理消息,
那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,所以導致該Activity的內存資源無法及時回收,引發內存泄漏,

正確做法爲:

public class MainActivity extends AppCompatActivity {   
private MyHandler mHandler = new MyHandler(this);   
private TextView mTextView      

//①將Handler改成靜態內部類
 private static class MyHandler extends Handler {        

//②將需要引用Activity的地方,改成弱引用
private WeakReference<Context> reference;    
public MyHandler(Context context) {            
reference = new WeakReference<>(context);      
}        

@Override      
public void handleMessage(Message msg) {        
MainActivity activity = (MainActivity) reference.get();        
if(activity != null){               
activity.mTextView.setText("");       
}     
} 
}

@Override      
protected void onCreate(Bundle savedInstanceState) {      
super.onCreate(savedInstanceState);        
setContentView(R.layout.activity_main);      
mTextView = (TextView)findViewById(R.id.textview);       
loadData();   
}     
private void loadData() {       
//...request         
Message message = Message.obtain();         
mHandler.sendMessage(message);   
} 

//③在onDestory()方法中移除handler發送的消息和任務
@Override     
protected void onDestroy() {       
super.onDestroy();       
mHandler.removeCallbacksAndMessages(null);  
} 

} 

使用mHandler.removeCallbacksAndMessages(null);是移除消息隊列中所有消息和所有的Runnable。
當然也可以使用 mHandler.removeCallbacks();或mHandler.removeMessages();來移除指定的Runnable和Message。

下面幾個方法都可以移除 Message:

public final void removeCallbacks(Runnable r);

public final void removeCallbacks(Runnable r, Object token);

public final void removeCallbacksAndMessages(Object token);

public final void removeMessages(int what);

public final void removeMessages(int what, Object object);

線程造成的內存泄漏

對於線程造成的內存泄漏,也是平時比較常見的,如下這兩個示例可能每個人都這樣寫過:

//——————test1     

new AsyncTask<Void, Void, Void>() {             

@Override              
protected Void doInBackground(Void... params) {           

SystemClock.sleep(10000);               
return null;            
}       
}.execute(); 


//——————test2         
new Thread(new Runnable() {         
@Override             
public void run() {             
SystemClock.sleep(10000);         
}       
}).start();


上面的異步任務和Runnable都是一個匿名內部類,因此它們對當前Activity都有一個隱式引用。如果Activity在銷燬之前,任務還未完成, 那麼將導致Activity的內存資源無法回收,造成內存泄漏。

正確的做法還是使用靜態內部類的方式(或將AsyncTask和Runnable類獨立出來),

如下:  
static class MyAsyncTask extends AsyncTask<Void, Void, Void> {    

private WeakReference<Context> weakReference;        

public MyAsyncTask(Context context) {         

weakReference = new WeakReference<>(context);   
}             

@Override 
protected Void doInBackground(Void... params) {           
SystemClock.sleep(10000);           
return null;       
}             

@Override          
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);             

MainActivity activity = (MainActivity) weakReference.get();         

if (activity != null) {                 
//...             }      
}    
}      

static class MyRunnable implements Runnable{         

@Override         
public void run() {              
SystemClock.sleep(10000);        
}    
}  

//——————      new Thread(new MyRunnable()).start();
new MyAsyncTask(this).execute(); 

這樣就避免了Activity的內存資源泄漏,當然在Activity銷燬時候也應該取消相應的任務AsyncTask::cancel(),避免任務在後臺執行浪費資源。

資源未關閉造成的內存泄漏

對於使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,以及一些開源項目的使用,應該在Activity銷燬時及時關閉或者註銷,否則這些資源將不會被回收,造成內存泄漏。

使用ListView時造成的內存泄漏(RecycleView同理)

初始時ListView會從BaseAdapter中根據當前的屏幕布局實例化一定數量的View對象,同時ListView會將這些View對象緩存起來。當向上滾動ListView時,原先位於最上面的Item的View對象會被回收,然後被用來構造新出現在下面的Item。這個構造過程就是由getView()方法完成的,getView()的第二個形參convertView就是被緩存起來的Item的View對象(初始化時緩存中沒有View對象則convertView是null)。

說白了,即在構造Adapter時,沒有使用緩存的convertView。
解決方法:在構造Adapter時,使用緩存的convertView。

集合容器中的內存泄露(靜態的集合類同理)

我們通常把一些對象的引用加入到了集合容器(比如ArrayList)中,當我們不需要該對象時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。

解決方法:在退出程序之前,將集合裏的東西clear,然後置爲null,再退出程序。
如: 使用 array.clear() ; array = null

WebView造成的泄露

當我們不要使用WebView對象時,應該調用它的destory()函數來銷燬它,並釋放其佔用的內存,否則其長期佔用的內存也不能被回收,從而造成內存泄露。

解決方法:爲WebView另外開啓一個進程,通過AIDL與主線程進行通信,WebView所在的進程可以根據業務的需要選擇合適的時機進行銷燬,從而達到內存的完整釋放。

MVP框架的內存泄漏問題:

當Modle在獲取數據時,不做處理,它就一直持有Presenter對象,而Presenter對象又持有Activity對象,這條GC鏈不剪斷,Activity就無法被完整回收。

換句話說:Presenter不銷燬,Activity就無法正常被回收。

解決:

Presenter在Activity的onDestroy方法回調時執行資源釋放操作,或者在Presenter引用View對象時使用更加容易回收的軟引用,弱應用。 

比如示例代碼: 
Activity

@Override
    public void onDestroy() {
        super.onDestroy();
        mPresenter.destroy();
        mPresenter = null;
    }

Presenter

public void destroy() {
    view = null;
    if(modle != null) {
        modle.cancleTasks();
        modle = null;
    }
}

Modle

public void cancleTasks() {
    // TODO 終止線程池ThreadPool.shutDown(),AsyncTask.cancle(),或者調用框架的取消任務api
}

View造成的內存泄漏

一般引用到佈局文件的View,如自定義控件,系統的Widget組件,Fragment等,就等於關聯上了當前的Activity,如果某個地方被static關鍵字修飾了,就成造成內存泄漏

解決辦法:
在Fragment,或Activity的onDestory()將該View致空

注意:RecycleView 使用到Adapter,Adapter也會間接引用到Activity,因此也需要致空


 @Override
    public void onDestroy() {
        super.onDestroy();
        mFloatingActionButton=null;
        mRecyclerView=null;
    }

Listener監聽器引起的內存泄漏:

不管是Android系統的Listener,還是自定義接口回調,定義Listener的類持有一個引用,調用Listener的類也持有了一個引用,系統GC不掉,就造成內泄漏了

解決:使用完listener完,將Listener致空

以百度定位爲例:

/***
 *  獲取定位成功
 */
 private void getLocationSuccess(BDLocation bdLocation){
     if(mLocationGetListener != null){
         mLocationGetListener.queryLocationSuccess(bdLocation);
         //致空,防止內存泄漏
         mLocationGetListener = null;
     }
 }

代碼不規範造成的內存溢出:

比如:

  • Bitmap 沒調用 recycle()方法,對於 Bitmap 對象在不使用時,我們應該先調用 recycle() 釋放內存,然後才它設置爲 null. 因爲加載 Bitmap 對象的內存空間,一部分是 java 的,一部分 C 的(因爲 Bitmap 分配的底層是通過 JNI 調用的 )。 而這個 recyle() 就是針對 C 部分的內存釋放

  • 構造 Adapter 時,沒有使用緩存的 convertView ,每次都在創建新的 converView。這裏推薦使用 ViewHolder

圖片放置在錯誤目錄造成的內存溢出:

  • 在Android 2.1以後,我們的工程目錄出現了drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi等目錄,這幾個目錄裏的圖片資源會按一定比例壓縮或擴充,如上比例爲3:4:6:8:12

  • 因此,如果我們將較大的圖片放在高分辨率的文件目錄裏,那麼真機或模擬器運行時,就會根據手機屏幕分辨率按一定比例壓縮圖片

  • 反之,如果把較大的圖片放在較小的目錄,就會造成內存泄漏

內存抖動:

就是突然申請很多對象,變量或者內存空間,突然又不用了,過一陣子又申請很多,系統GC不過來,就會影響系統流暢性(說白了就是內存使用不穩定)

一般不正確的定義變量引起的:

如:

for(int = 0;i<10000;i++){
    Person p=new Person();  //不斷在循環裏 new 對象
}

解決: 把Person p=new Person(); 抽到成員變量

頻繁的findViewByID引起的內存泄漏:

findViewByID時獲得的View,會佔用部分內存,頻繁的調用findViewById又不釋放這部分內存,久而久之就會造成內存泄漏

解決:合理的使用單例,或使用viewHolder,寫代碼儘量避免重複使用findViewById的情況


實際開發中,如何避免內存泄漏:

  1. 在使用Context 時,優先考慮生命週期長的Application的Context
  2. 對於需要在靜態內部類中使用非靜態外部成員變量(如:Context、View ),可以在靜態內部類中使用弱引用來引用外部類的變量來避免內存泄漏。
  3. 對於不再需要使用的對象,將其賦值爲null,比如使用完Bitmap後先調用recycle(),再賦爲null。
  4. 保持對對象生命週期的敏感,特別注意單例、靜態對象、全局性集合等的生命週期。
  5. 對於生命週期比Activity長的內部類對象,並且內部類中使用了外部類的成員變量,可以這樣做避免內存泄漏:
    1. 將內部類改爲靜態內部類
    2. 靜態內部類中使用弱引用來引用外部類的成員變量
  6. 注意Handler對象和Thread的代碼編寫規範
  7. Context裏如果有線程,一定要在onDestroy()裏及時停掉
  8. 如果某個View 或 Adapter發生內存泄漏,那麼我們可以在個Activty或Fragment 的onDestory() 把該View 設置爲null,如recycleView=null;
  9. 部分系統造成的內存泄漏可以不予理會,如 InputManager
  10. static 關鍵字儘量避免使用,一般情況下GC不會回收靜態變量
  11. 使用單例,要用到Context,建議這樣寫 context.getApplication();
  12. 將wdight或自定義控件的View,在onDestory() 致空
  13. 避免代碼設計模式的錯誤造成內存泄露;譬如循環引用,A持有B,B持有C,C持有A,還有類似MVP架構,Listener監聽器,這樣的設計誰都得不到釋放。
  14. 頻繁的字符串拼接用StringBuilder(字符串通過+的方式進行字符串拼接,會產生中間字符串內存塊,這些都是沒用的)
  15. 複用系統自帶的資源,如不同的佈局文件,控件ID名稱一樣
  16. 避免在onDraw()方法裏面執行對象
  17. 避免在循環裏反覆創建對象
  18. 規範的將資源文件放在合適的工程目錄
  19. 避免頻繁的調用findViewById

獲取App在當前手機系統能達到的最大內存(小擴展)


/**
     * 獲取應用最高可用內存
     *
     * @return 最大內存
     */
    public static long getDeviceMaxMemory() {

        return Runtime.getRuntime().maxMemory() / 1024;
    }

 /**
     * 獲取App在當前手機系統的最大內存
     *
     * @return 最大內存
     */
    public static String getAppMemoryClass_Rom(Context context) {
        StringBuilder sb = new StringBuilder();
        ActivityManager activityManager = (ActivityManager) context
                .getSystemService(Context.ACTIVITY_SERVICE);
        int memoryClass = activityManager.getMemoryClass();
        int largeMemoryClass = activityManager.getLargeMemoryClass();
        sb.append("memoryClass:"+memoryClass+"\n");
        sb.append("largeMemoryClass:"+largeMemoryClass+"\n");
        return sb.toString();
    }

如果想進一步加深 Context 理解 可看:

不牆要加載略慢,能牆使用更佳

國外Context文檔

總結:

  • 本人的這篇文章估計總結的非常詳細了,在以後工作中如果發現有新的內存泄漏情況會繼續更新

  • 如果覺得文章對您有用,點擊一個關注憋

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章