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空間 完全由程序員控制,我們使用mallocC++ 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項都是在內存中的

參考文章:
Android進程的內存管理分析
Android內存泄漏分析及調試

發佈了62 篇原創文章 · 獲贊 54 · 訪問量 55萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章