1. 程式人生 > >Android OOM:記憶體管理分析和記憶體洩露原因總結

Android OOM:記憶體管理分析和記憶體洩露原因總結

一、Android程序的記憶體管理分析

1. 程序的地址空間

在32位作業系統中,程序的地址空間為0到4GB,示意圖如下:
這裡寫圖片描述
這裡主要說明一下Stack和Heap:

  • Stack空間:(進棧和出棧)由作業系統控制,其中主要儲存 函式地址函式引數區域性變數 等等。
    所以Stack空間不需要很大,一般為幾MB大小。
  • Heap空間:使用由程式設計師控制,程式設計師可以使用malloc、new、free、delete等函式呼叫來操作這片地址空間。
    Heap為程式完成各種複雜任務提供記憶體空間,所以空間比較大,一般為幾百MB到幾GB。
    正是因為Heap空間由程式設計師管理,所以容易出現使用不當導致嚴重問題。

2. 程序記憶體空間和RAM之間的關係

  • 程序的記憶體空間只是 虛擬記憶體,而程式的執行需要的是實實在在的記憶體,即 實體記憶體(RAM)
    在必要時,作業系統會將程式執行中申請的記憶體(虛擬記憶體)對映到RAM,讓程序能夠使用實體記憶體。
  • 另外,RAM的一部分被作業系統留作他用,比如視訊記憶體 等等,記憶體對映和視訊記憶體等都是由作業系統控制,我們也不必過多地關注它,程序所操作的空間都是虛擬地址空間,無法直接操作RAM

3. Android中的程序

  1. native程序:採用C/C++實現,不包含dalvik例項的linux程序**,/system/bin/目錄下面的程式檔案執行後都是以native程序形式存在的。比如/system/bin/surfaceflinger

    /system/bin/rildprocrank等就是native程序。

  2. java程序:例項化了dalvik虛擬機器例項的linux程序,程序的入口main函式為java函式。 dalvik虛擬機器例項的宿主程序是fork()系統呼叫建立的linux程序,所以每一個Android上的java程序實際上就是一個linux程序,只是程序中多了一個dalvik虛擬機器例項。因此,java程序的記憶體分配比native程序複雜。Android系統中的應用程式基本都是java程序,如桌面電話聯絡人狀態列等等。

4. Android中程序的堆記憶體

  • heap空間 完全由程式設計師控制,我們使用malloc
    C++ newjava new所申請的空間都是heap空間
    , C/C++申請的記憶體空間在native heap中,而java申請的記憶體空間則在dalvik heap中。

5. Android的 java程式為什麼容易出現OOM

  • 因為Android系統對dalvik的vm heapsize作了硬性限制,當java程序申請的java空間超過閾值時,就會丟擲OOM異常(這個閾值可以是48M、24M、16M等,視機型而定),可以通過adb shell getprop | grep dalvik.vm.heapgrowthlimit 檢視此值。

  • 也就是說,程式發生OMM並不表示RAM不足,而是因為程式申請的java heap物件超過了dalvik vm heapgrowthlimit。也就是說,在RAM充足的情況下,也可能發生OOM。

  • 這樣設計的 目的是為了讓Android系統能同時讓比較多的程序常駐記憶體,這樣程式啟動時就不用每次都重新載入到記憶體,能夠給使用者更快的響應

6. Android如何應對RAM不足

java程式發生OMM並不是表示RAM不足,如果RAM真的不足,會發生什麼呢? 這時Android的 memory killer 會起作用,當RAM所剩不多時,memory killer會殺死一些優先順序比較低的程序來釋放實體記憶體,讓高優先順序程式得到更多的記憶體。我們在分析log時,看到的程序被殺的log。

  Process com.xxx.xxxx(pid xxxx) has died. 

7. 應用程式如何繞過dalvikvm heapsize的限制

對於一些大型的應用程式(比如遊戲),記憶體使用會比較多,很容易超超出vm heapsize的限制,這時怎麼保證程式不會因為OOM而崩潰呢?

  1. 建立子程序

    • 建立一個新的程序,那麼我們就可以把一些物件分配到新程序的heap上了,從而 達到一個應用程式使用更多的記憶體的目的,當然,建立子程序會增加系統開銷,而且並不是所有應用程式都適合這樣做,視需求而定。
    • 建立子程序的方法:使用android:process標籤
  2. 使用jni在 native heap 上申請空間(推薦使用)

    • 因為 native heap 的增長並不受 dalvik vm heapsize 的限制。
    • 只要RAM有剩餘空間,程式設計師可以一直在native heap上申請空間,當然如果 RAM快耗盡,memory killer 會殺程序釋放 RAM。
    • 我們在使用一些軟體時,有時候會閃退,就可能是軟體在native層申請了比較多的記憶體導致的。比如 UC web 在瀏覽內容比較多的網頁時可能閃退,原因就是其native heap增長到比較大的值,佔用了大量的 RAM,被memory killer殺掉了。
  3. 使用視訊記憶體(作業系統預留RAM的一部分作為視訊記憶體)
    • 使用OpenGL textures等API,texture memory不受dalvik vm heapsize限制,這個沒實踐過。
    • 再比如Android中的GraphicBufferAllocator申請的記憶體就是視訊記憶體。

8. java程式如何才能建立native物件

必須使用 jni,而且應該用C語言的malloc或者C++的new關鍵字。
例項程式碼如下:

    JNIEXPORT void JNICALLJava_com_example_demo_TestMemory_nativeMalloc(JNIEnv *, jobject)  
    {    
             void *p= malloc(1024*1024*);
             SLOGD("allocate 50M Bytes memory");  
             if (p !=NULL)  
             {         
                 //memorywill not used without calling memset()  
                 memset(p,0, 1024*1024*50);  
             }  else   SLOGE("mallocfailure.");  
       ...
       ...
    free(p); // free memory  
    }  

或者:

    JNIEXPORT voidJNICALL Java_com_example_demo_TestMemory_nativeMalloc(JNIEnv *, jobject)  
    {     
             SLOGD("allocate 50M Bytesmemory");  
             char *p = new char[1024 * 1024 * 50];  
             if (p != NULL)  
             {         
                 //memory will not usedwithout calling memset()  
                 memset(p, 1, 1024*1024*50);  
             } else  SLOGE("newobject failure.");  
      ...
      ...
    free(p); //free memory  
    }  

malloc或者new申請的記憶體是虛擬記憶體,申請之後不會立即對映到實體記憶體,即不會佔用RAM。只有呼叫memset使用記憶體後,虛擬記憶體才會真正對映到RAM。

9. 明明還有很多記憶體,但是發生OOM了。。

  • 這種情況經常出現在生成Bitmap的時候。
  • 在一個函式裡生成一個13m 的int陣列,再該函式結束後,按理說這個int陣列應該已經被釋放了,或者說可以釋放,這個13M的空間應該可以空出來。
  • 這個時候如果你繼續生成一個10M的int陣列是沒有問題的,反而生成一個4M的Bitmap就會跳出OOM。這個就奇怪了,為什麼10M的int夠空間,反而4M的Bitmap不夠呢?

在Android中:

  1. 一個程序的記憶體可以由2個部分組成:java 使用記憶體 ,C 使用記憶體
    這兩個記憶體的和必須小於16M,不然就會出現大家熟悉的OOM,這個就是第一種OOM的情況。
  2. 一旦記憶體分配給Java後,以後這塊記憶體即使釋放後,也只能給Java的使用
    這個估計跟java虛擬機器裡把記憶體分成好幾塊進行快取的原因有關,反正C就別想用到這塊的記憶體了,所以如果Java突然佔用了一個大塊記憶體,即使很快釋放了:
    • C 能使用的記憶體 = 16M - Java某一瞬間佔用的最大記憶體
    • Bitmap的生成是通過malloc進行記憶體分配的,佔用的是C的記憶體,這個也就說明了,上述的4MBitmap無法生成的原因,因為在13M被Java用過後,剩下C能用的只有3M了。

二、瞭解dalvik的Garbage Collection

如圖所示:
這裡寫圖片描述

  • GC會選擇一些它瞭解 還存活的物件 作為 記憶體遍歷的根節點GC Roots),比方說thread stack中的變數JNI中的全域性變數zygote中的物件(class loader載入)等,然後開始對heap進行遍歷。到最後,部分沒有直接或者間接引用到GC Roots的就是需要回收的垃圾,會被GC回收掉
    如下圖藍色部分。
    這裡寫圖片描述

三、常見的記憶體洩漏

1. 非靜態內部類 的靜態例項 容易造成記憶體洩漏

    public class MainActivity extends Activity  
    {  
        // 非靜態內部類的靜態例項
        static Demo sInstance = null;  

        @Override  
        public void onCreate(BundlesavedInstanceState)  
        {  
            super.onCreate(savedInstanceState);  
            setContentView(R.layout.activity_main);  
            if (sInstance == null)  {  
               sInstance= new Demo();  
            }  
        }  

        class Demo  
        {  
            void doSomething()  
            {  
                System.out.print("dosth.");  
            }  
        }  
    }  
  • 上面的程式碼中的 sInstance 例項 型別為靜態例項,在第一個MainActivity act1例項建立時,sInstance會獲得並一直持有act1的引用。
  • 當MainAcitivity銷燬後重建,因為sInstance持有act1的引用,所以act1是無法被GC回收的,程序中會存在2個MainActivity例項(act1和重建後的MainActivity例項),這個act1物件就是一個無用的但一直佔用記憶體的物件,即無法回收的垃圾物件。
  • 所以,對於lauchMode不是singleInstance的Activity, 應該避免在activity裡面例項化其非靜態內部類的靜態例項

2. Activity使用靜態成員

    private static Drawable sBackground;   

    @Override    
    protected void onCreate(Bundle state) {    
        super.onCreate(state);    

        TextView label = new TextView(this);    
        label.setText("Leaks are bad");    

        if (sBackground == null) {    
            sBackground = getDrawable(R.drawable.large_bitmap);    
        }    
        label.setBackgroundDrawable(sBackground);    

        setContentView(label);    
    }   
  • 由於用 靜態成員sBackground 快取了drawable物件,所以activity載入速度會加快,但是這樣做是錯誤的。因為在android 2.3系統上,它會導致activity銷燬後無法被系統回收。

label .setBackgroundDrawable()呼叫會將label賦值給sBackground的成員變數 mCallback
上面程式碼意味著:sBackground(GC Root)會持有TextView物件,而TextView持有Activity物件。所以導致Activity物件無法被系統回收。

下面看看android4.0為了避免上述問題所做的改進。

  • 先看看android 2.3的Drawable.Java對setCallback的實現:
    public final void setCallback(Callback cb){
        mCallback = cb;
    }

// 在android 2.3中要避免記憶體洩漏也是可以做到的,
// 在activity的onDestroy時呼叫
// sBackgroundDrawable.setCallback(null)。
  • 再看看android 4.0的Drawable.Java對setCallback的實現:
    public final void setCallback(Callback cb){
        mCallback = newWeakReference<Callback> (cb);
    }

以上2個例子的記憶體洩漏都是因為 Activity的 引用的生命週期 超越了Activity 物件的生命週期。也就是常說的 Context洩漏,因為activity就是context。

3. 避免context相關的記憶體洩漏,需要注意以下幾點

  • 不要對activity的context長期引用
    ( 一個activity的引用的生存週期應該和activity的生命週期相同 )

  • 如果可以的話,儘量使用關於application的context來替代和activity相關的context

  • 如果一個acitivity的非靜態內部類的生命週期不受控制,那麼避免使用它;正確的方法是 使用一個靜態的內部類,並且對它的外部類有一WeakReference,就像在ViewRootImpl中內部類W所做的那樣

4. 使用handler時的記憶體問題

1) 我們知道,Handler通過傳送Message與主執行緒互動。

  • Message發出之後是儲存在MessageQueue中的,有些Message也不是馬上就被處理的。
  • 在Message中存在一個 target,是Handler的一個引用,如果Message在Queue中存在的時間越長,就會導致Handler無法被回收。
  • 如果Handler是非靜態的,則會導致Activity或者Service不會被回收。 所以正確處理Handler等之類的內部類,應該將自己的Handler定義為靜態內部類

2) HandlerThread的使用也需要注意:

  • 當我們在activity裡面建立了一個HandlerThread,程式碼如下:
    public classMainActivity extends Activity  
    {  
        @Override  
        public void onCreate(BundlesavedInstanceState)  
        {  
            super.onCreate(savedInstanceState);  
            setContentView(R.layout.activity_main);  
            Thread mThread = newHandlerThread("demo", Process.THREAD_PRIORITY_BACKGROUND);   
            mThread.start();  
            MyHandler mHandler = new MyHandler( mThread.getLooper( ) );  
            ...
            ... 
        }  

        @Override  
        public void onDestroy()  
        {  
            super.onDestroy(); 
            // mThread.getLooper().quit(); 
        }  
    }  
  • 這個程式碼存在洩漏問題,因為 HandlerThread的run方法是一個死迴圈,它不會自己結束,執行緒的生命週期超過了activity生命週期,當橫豎屏切換,HandlerThread執行緒的數量會隨著activity重建次數的增加而增加。

  • 應該在onDestroy時將執行緒停止掉:mThread.getLooper().quit();

另外,對於不是HandlerThread的執行緒,也應該確保activity消耗後,執行緒已經終止,可以這樣做:在onDestroy時呼叫 mThread.join();

join( ) 的作用是:“等待該執行緒終止”,這裡需要理解的就是該執行緒是指的主執行緒等待子執行緒的終止。也就是:在子執行緒呼叫了join()方法後面的程式碼,只有等到子執行緒結束了才能執行。

5. 註冊某個物件後未反註冊

比如 註冊廣播接收器註冊觀察者 等等。

  • 假設我們希望在鎖屏介面(LockScreen)中,監聽系統中的電話服務以獲取一些資訊(如訊號強度等),則可以在LockScreen中定義一個PhoneStateListener的物件,同時將它 註冊TelephonyManager服務中。對於LockScreen物件,當需要顯示鎖屏介面的時候就會建立一個LockScreen物件,而當鎖屏介面消失的時候LockScreen物件就會被釋放掉。

  • 但是如果 在釋放LockScreen物件的時候忘記取消我們之前註冊的PhoneStateListener物件,則會導致LockScreen無法被GC回收。如果不斷的使鎖屏介面顯示和消失,則最終會由於大量的LockScreen物件沒有辦法被回收而引起OutOfMemory,使得system_process程序掛掉。

雖然有些系統程式,它本身好像是可以自動取消註冊的(當然不及時),但是我們還是 應該在我們的程式中明確的取消註冊,程式結束時應該把所有的註冊都取消掉。

6. 集合中物件沒清理造成的記憶體洩露

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

  • 比如某公司的ROM的鎖屏曾經就存在記憶體洩漏問題:
  • 這個洩漏是因為LockScreen**每次顯示時會註冊幾個callback**,它們儲存在
    KeyguardUpdateMonitor的ArrayList<InfoCallback>
    ArrayList<SimStateCallback>
    等ArrayList例項中。但是在LockScreen**解鎖後,這些callback沒有被remove掉**,導致ArrayList不斷增大, callback物件不斷增多。這些callback物件的size並不大,heap增長比較緩慢,需要長時間地使用手機才能出現OOM,由於鎖屏是駐留在system_server程序裡,所以導致結果是手機重啟。

7. 資源物件沒關閉造成的記憶體洩露

  • 資源性物件 比如(CursorFile檔案等) 往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收記憶體。它們的緩衝不僅存在於Java虛擬機器內,還存在於Java虛擬機器外。
  • 如果我們僅僅是把它的引用設定為null,而不關閉它們,往往會造成記憶體洩露。因為有些資源性物件,比如SQLiteCursor(在解構函式finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性物件在不使用的時候,應該立即呼叫它的close()函式,將其關閉掉,然後再置為null.
  • 在我們的程式退出時一定要確保我們的資源性物件已經關閉

8. 一些不良程式碼成記憶體壓力

有些程式碼並不造成記憶體洩露,但是它們或是 對不使用的記憶體沒進行有效及時的釋放,或是沒有有效的利用已有的物件而是頻繁的申請新記憶體,對記憶體的回收和分配造成很大影響的。

1) Bitmap使用不當
  • 及時的銷燬
    在用完Bitmap時,要及時的bitmap.recycle( )掉。
    注意,recycle( )並不能確定立即就會將Bitmap釋放掉,但是會給虛擬機器一個暗示:“該圖片可以釋放了”。
  • 設定取樣率
    有時候,我們要顯示的區域很小,沒有必要將整個圖片都加載出來,而只需要記載一個縮小過的圖片,這時候可以設定一定的取樣率,那麼就可以大大減小佔用的記憶體。如下面的程式碼:
    private ImageView preview;    
    BitmapFactory.Options options = newBitmapFactory.Options();  
    // 圖片寬高都為原來的二分之一,即圖片為原來的四分之一   
    options.inSampleSize = 2;

    Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), 
          null, options); preview.setImageBitmap(bitmap);   
  • 巧妙的運用軟引用(SoftRefrence)
    有些時候,我們使用Bitmap後沒有保留對它的引用,因此就無法呼叫Recycle函式。這時候巧妙的運用軟引用,可以使Bitmap在記憶體快不足時得到有效的釋放。如下:
    SoftReference<Bitmap>  bitmap_ref  = new SoftReference<Bitmap>(
             BitmapFactory.decodeStream(inputstream));   
    ... 
    ...
    if (bitmap_ref .get() != null) { 
          bitmap_ref.get().recycle();  
    }          
2) 構造Adapter時,沒有使用快取的 convertView
  • 初始時ListView會從BaseAdapter中根據當前的屏幕布局例項化一定數量的view物件,同時ListView會將這些view物件快取起來。

  • 當向上滾動ListView時,原先位於最上面的list item的view物件會被回收,然後被用來構造新出現的最下面的list item。這個構造過程就是由getView()方法完成的,getView()的第二個形參 View convertView就是被快取起來的list item的view物件 ( 初始化時快取中沒有 view 物件,則 convertView 是 null )。

由此可以看出,如果我們不去使用convertView,而是每次都在getView()中重新例項化一個View物件的話,即浪費時間,也造成記憶體垃圾,給垃圾回收增加壓力,如果垃圾回收來不及的話,虛擬機器將不得不給該應用程序分配更多的記憶體,造成不必要的記憶體開支。

3) 不要在經常呼叫的方法中建立物件,尤其是忌諱在迴圈中建立物件。

可以適當的使用 hashtablevector 建立一組物件容器,然後從容器中去取那些物件,而不用每次 new 之後又丟棄。

9. 查詢資料庫而沒有關閉Cursor

在Android中,Cursor是很常用的一個物件,但在寫程式碼時,經常會有人忘記呼叫close, 或者因為程式碼邏輯問題狀況導致close未被呼叫

  • 通常,在Activity中,我們可以呼叫startManagingCursor或直接使用managedQuery讓Activity自動管理Cursor物件。
    但需要注意的是,當Activity結束後,Cursor將不再可用!
  • 若操作Cursor的程式碼和UI不同步(如後臺執行緒),需要先判斷Activity是否已經結束,或者在呼叫OnDestroy前,先等待後臺執行緒結束。
  • 除此之外,以下也是比較常見的Cursor不會被關閉的情況:
try {  
    Cursor c = queryCursor();  
    int a = c.getInt(1);  
    ......  
    c.close();  
} catch (Exception e) {  
} 
// 雖然表面看起來,Cursor.close()已經被呼叫
// 但若出現異常,將會跳過close(),從而導致記憶體洩露。
// 所以,我們的程式碼應該以如下的方式編寫:

Cursor c = queryCursor();  
try {      
    int a = c.getInt(1);  
    ......  
} catch (Exception e) {  
} finally {  
    c.close(); // 在finally中呼叫close(), 保證其一定會被呼叫   
} 

10. 呼叫registerReceiver後未呼叫unregisterReceiver()

在呼叫registerReceiver後,若未呼叫unregisterReceiver,其所佔的記憶體是相當大的。
而我們經常可以看到類似於如下的程式碼:

registerReceiver(new BroadcastReceiver() {  
    ...  
}, filter); ...

這是個很嚴重的錯誤,因為它會導致BroadcastReceiver不會被unregister而導致記憶體洩露。

11. WebView物件沒有銷燬

當我們不要使用WebView物件時,應該呼叫它的destory()函式來銷燬它,並釋放其佔用的記憶體,否則其佔用的記憶體長期也不能被回收,從而造成記憶體洩露。

12. GridView的濫用

GridView和ListView的實現方式不太一樣。GridView的View不是即時建立的,而是全部儲存在記憶體中的。比如一個GridView有100項,雖然我們只能看到10項,但是其實整個100項都是在記憶體中的