Android OOM全解析

Android oom 有時出現很頻繁,這一般不是Android設計的問題,一般是我們的問題。

  就我的經驗而言,出現oom,無非主要是以下幾個方面:

  一、加載對象過大

  二、相應資源過多,沒有來不及釋放。

  解決這樣的問題,也有一下幾個方面:

  一:在內存引用上做些處理,常用的有軟引用、強化引用、弱引用

  二:在內存中加載圖片時直接在內存中做處理,如:邊界壓縮.
  三:動態回收內存
  四:優化Dalvik虛擬機的堆內存分配
  五:自定義堆內存大小

  可真有這麼簡單嗎,不見得,看我娓娓道來:

  軟引用(SoftReference)、虛引用(PhantomRefrence)、弱引用(WeakReference),這三個類是對heap中java對象的應用,通過這個三個類可以和gc做簡單的交互,除了這三個以外還有一個是最常用的強引用.

  強引用,例如下面代碼:

  Object o=new Object();       
  Object o1=o;   

  上面代碼中第一句是在heap堆中創建新的Object對象通過o引用這個對象,第二句是通過o建立o1到new Object()這個heap堆中的對象的引用,這兩個引用都是強引用.只要存在對heap中對象的引用,gc就不會收集該對象.如果通過如下代碼:

  o=null;         

  o1=null;

   heap中對象有強可及對象、軟可及對象、弱可及對象、虛可及對象和不可到達對象。應用的強弱順序是強、軟、弱、和虛。對於對象是屬於哪種可及的對象,由他的最強的引用決定。如下:

複製代碼
String abc=new String("abc");  //1       
SoftReference<String> abcSoftRef=new SoftReference<String>(abc);  //2       
WeakReference<String> abcWeakRef = new WeakReference<String>(abc); //3       
abc=null//4       
abcSoftRef.clear();//5在此例中,透過 get() 可以取得此 Reference 的所指到的對象,如果返回值爲 null 的話,代表此對象已經被清除。這類的技巧,在設計 Optimizer 或 Debugger 這類的程序時常會用到,因爲這類程序需要取得某對象的信息,但是不可以 影響此對象的垃圾收集。
複製代碼

   虛引用
   就是沒有的意思,建立虛引用之後通過get方法返回結果始終爲null,通過源代碼你會發現,虛引用通向會把引用的對象寫進referent,只是get方法返回結果爲null.先看一下和gc交互的過程在說一下他的作用. 
     不把referent設置爲null, 直接把heap中的new String("abc")對象設置爲可結束的(finalizable).
      與軟引用和弱引用不同, 先把PhantomRefrence對象添加到它的ReferenceQueue中.然後在釋放虛可及的對象. 你會發現在收集heap中的new String("abc")對象之前,你就可以做一些其他的事情.通過以下代碼可以瞭解他的作用.

  雖然這些常見引用,能夠使其gc回收,但是gc又不是非常的智能了,因而oom亂免。

  二:在內存中加載圖片時直接在內存中做處理。

  儘量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource來設置一張大圖,因爲這些函數在完成decode後,最終都是通過java層的createBitmap來完成的,需要消耗更多內存。

  因此,改用先通過BitmapFactory.decodeStream方法,創建出一個bitmap,再將其設爲ImageView的 source,decodeStream最大的祕密在於其直接調用JNI>>nativeDecodeAsset()來完成decode,無需再使用java層的createBitmap,從而節省了java層的空間。

  如果在讀取時加上圖片的Config參數,可以跟有效減少加載的內存,從而跟有效阻止拋out of Memory異常,另外,decodeStream直接拿的圖片來讀取字節碼了, 不會根據機器的各種分辨率來自動適應, 使用了decodeStream之後,需要在hdpi和mdpi,ldpi中配置相應的圖片資源, 否則在不同分辨率機器上都是同樣大小(像素點數量),顯示出來的大小就不對了。

  另外,以下方式也大有幫助:

複製代碼
InputStream is = this.getResources().openRawResource(R.drawable.pic1); 
     BitmapFactory.Options options=new BitmapFactory.Options(); 
     options.inJustDecodeBounds = false; 
     options.inSampleSize = 10;   //width,hight設爲原來的十分一 
     Bitmap btp =BitmapFactory.decodeStream(is,null,options); 
  
if(!bmp.isRecycle() ){ 
         bmp.recycle()   //回收圖片所佔的內存 
         system.gc()  //提醒系統及時回收 
} 
  
複製代碼

  以下奉上一個方法,以最省內存的方式讀取本地資源的圖片

複製代碼
/** 
 
*  
 
* @param context 
 
* @param resId 
 
* @return 
 
*/ 
 
 
 
public static Bitmap readBitMap(Context context, int resId){ 
 
 
 
BitmapFactory.Options opt = new BitmapFactory.Options(); 
 
 
 
opt.inPreferredConfig = Bitmap.Config.RGB_565; 
 
 
 
opt.inPurgeable = true; 
 
 
 
opt.inInputShareable = true; 
 
 
 
//獲取資源圖片 
 
 
 
InputStream is = context.getResources().openRawResource(resId); 
 
 
 
return BitmapFactory.decodeStream(is,null,opt); 
 
 
 
}
複製代碼

 

昨天在模擬器上給gallery放入圖片的時候,出現java.lang.OutOfMemoryError: bitmap size exceeds VM budget 異常,圖像大小超過了RAM內存。 
 
      模擬器RAM比較小,只有8M內存,當我放入的大量的圖片(每個100多K左右),就出現上面的原因。 
由於每張圖片先前是壓縮的情況,放入到Bitmap的時候,大小會變大,導致超出RAM內存,具體解決辦法如下: 
 

複製代碼
```java 
//解決加載圖片 內存溢出的問題 
                    //Options 只保存圖片尺寸大小,不保存圖片到內存 
                BitmapFactory.Options opts = new BitmapFactory.Options(); 
                //縮放的比例,縮放是很難按準備的比例進行縮放的,其值表明縮放的倍數,SDK中建議其值是2的指數值,值越大會導致圖片不清晰 
                opts.inSampleSize = 4; 
                Bitmap bmp = null; 
                bmp = BitmapFactory.decodeResource(getResources(), mImageIds[position],opts);                              
 
                ...               
 
               //回收 
                bmp.recycle(); 
 
複製代碼

 


  
通過上面的方式解決了,但是這並不是最完美的解決方式。

通過一些瞭解,得知如下:

優化Dalvik虛擬機的堆內存分配

對於Android平臺來說,其託管層使用的Dalvik JavaVM從目前的表現來看還有很多地方可以優化處理,比如我們在開發一些大型遊戲或耗資源的應用中可能考慮手動干涉GC處理,使用dalvik.system.VMRuntime類提供的setTargetHeapUtilization方法可以增強程序堆內存的處理效率。當然具體原理我們可以參考開源工程,這裏我們僅說下使用方法: private final static floatTARGET_HEAP_UTILIZATION = 0.75f; 在程序onCreate時就可以調用VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);即可。

Android堆內存也可自己定義大小

對於一些Android項目,影響性能瓶頸的主要是Android自己內存管理機制問題,目前手機廠商對RAM都比較吝嗇,對於軟件的流暢性來說RAM對性能的影響十分敏感,除了 優化Dalvik虛擬機的堆內存分配外,我們還可以強制定義自己軟件的對內存大小,我們使用Dalvik提供的dalvik.system.VMRuntime類來設置最小堆內存爲例:

private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;

VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE); //設置最小heap內存爲6MB大小。當然對於內存吃緊來說還可以通過手動干涉GC去處理

bitmap 設置圖片尺寸,避免 內存溢出 OutOfMemoryError的優化方法


★android 中用bitmap 時很容易內存溢出,報如下錯誤:Java.lang.OutOfMemoryError : bitmap size exceeds VM budget

● 主要是加上這段:

BitmapFactory.Options options = new BitmapFactory.Options(); 
                options.inSampleSize = 2; 
  
● eg1:(通過Uri取圖片)

複製代碼
private ImageView preview; 
BitmapFactory.Options options = new BitmapFactory.Options(); 
                    options.inSampleSize = 2;//圖片寬高都爲原來的二分之一,即圖片爲原來的四分之一 
                    Bitmap bitmap = BitmapFactory.decodeStream(cr 
                            .openInputStream(uri), null, options); 
                    preview.setImageBitmap(bitmap); 
  
複製代碼

 


以上代碼可以優化內存溢出,但它只是改變圖片大小,並不能徹底解決內存溢出。
● eg2:(通過路徑去圖片)

複製代碼
private ImageView preview; 
private String fileName= "/sdcard/DCIM/Camera/2010-05-14 16.01.44.jpg"; 
BitmapFactory.Options options = new BitmapFactory.Options(); 
                options.inSampleSize = 2;//圖片寬高都爲原來的二分之一,即圖片爲原來的四分之一 
                        Bitmap b = BitmapFactory.decodeFile(fileName, options); 
                        preview.setImageBitmap(b); 
                        filePath.setText(fileName); 
複製代碼

    這樣,能夠壓縮足夠的比例,但是對於小內存手機,特別是那種16mheap的手機是避免不了了。

   三.動態分配內存

  動態內存管理DMM(Dynamic Memory Management)是從Heap中直接分配內存和回收內存。

  有兩種方法實現動態內存管理。

  一是顯示內存管理EMM(Explicit Memory Management)。
在EMM方式,內存從Heap中進行分配,用完後手動回收。程序使用malloc()函數分配整數數組,並使用free()函數釋放分配的內存。

  二是自動內存管理AMM(Automatic Memory Management)。
AMM也可叫垃圾回收器(Garbage Collection)。Java編程語言實現了AMM,與EMM不同,Run-time system關注已分配的內存空間,一旦不再使用,立即回收。

  無論是EMM還是AMM,所有的Heap管理計劃都面臨一些共同的問題和前在的缺陷:
  1)內部碎片(Internal Fragmentation)
當內存有浪費時,內部碎片出現。因爲內存請求可導致分配的內存塊過大。比如請求128字節的存儲空間,結果Run-time system分配了512字節。

  2)外部碎片(External Fragmentation)
當一系列的內存請求留下了數個有效的內存塊,但這些內存塊的大小均不能滿足新請求服務,此時出現外部碎片。

  3)基於定位的延遲(Location-based Latency)
延遲問題出現在兩個數據值存儲得相隔很遠,導致訪問時間增加。

  EMM往往比AMM更快。
  EMM與AMM比較表:
——————————————————————————————————————
                                 EMM                                        AMM
——————————————————————————————————————
Benefits     尺寸更小、速度更快、易控制         stay focused on domain issues
Costs        複雜、記賬、內存泄露、指針懸空           不錯的性能
——————————————————————————————————————

早期的垃圾回收器非常慢,往往佔用50%的執行時間。

垃圾回收器理論產生於1959年,Dan Edwards在Lisp編程語言的開發時實現了第一個垃圾回收器。

垃圾回收器有三種基本的經典算法:

1)Reference counting(引用計數)
基本思想是:當對象創建並賦值時該對象的引用計數器置1,每當對象給任意變量賦值時,引用記數+1;一旦退出作用域則引用記數-1。一旦引用記數變爲0,則該對象可以被垃圾回收。
引用記數有其相應的優勢:對程序的執行來說,每次操作只需要花費很小塊的時間。這對於不能被過長中斷的實時系統來說有着天然的優勢。
但也有其不足:不能夠檢測到環(兩個對象的互相引用);同時在每次增加或者減少引用記數的時候比較費時間。
在現代的垃圾回收算法中,引用記數已經不再使用。

2)Mark-sweep(標記清理)
基本思想是:每次從根集出發尋找所有的引用(稱爲活對象),每找到一個,則對其做出標記,當追蹤完成之後,所有的未標記對象便是需要回收的垃圾。
也叫追蹤算法,基於標記並清除。這個垃圾回收步驟分爲兩個階段:在標記階段,垃圾回收器遍歷整棵引用樹並標記每一個遇到的對象。在清除階段,未標記的對象被釋放,並使其在內存中可用。

3)Copying collection(複製收集)
基本思想是:將內存劃分爲兩塊,一塊是當前正在使用;另一塊是當前未用。每次分配時使用當前正在使用內存,當無可用內存時,對該區域內存進行標記,並將標記的對象全部拷貝到當前未用內存區,這是反轉兩區域,即當前可用區域變爲當前未用,而當前未用變爲當前可用,繼續執行該算法。
拷貝算法需要停止所有的程序活動,然後開始冗長而繁忙的copy工作。這點是其不利的地方。

近年來還有兩種算法:

1)Generational garbage collection(分代)
其思想依據是:
  (1) 被大多數程序創建的大多數對象有着非常短的生存期。
  (2) 被大多數程序創建的部分對象有着非常長的生存期。
簡單拷貝算法的主要不足是它們花費了更多的時間去拷貝了一些長期生存的對象。
而分代算法的基本思想是:將內存區域分兩塊(或更多),其中一塊代表年輕代,另一塊代表老的一代。針對不同的特點,對年輕一代的垃圾收集更爲頻繁,對老代的收集則較少,每次經過年輕一代的垃圾回收總會有未被收集的活對象,這些活對象經過收集之後會增加成熟度,當成熟度到達一定程度,則將其放進老代內存塊中。
分代算法很好的實現了垃圾回收的動態性,同時避免了內存碎片,是目前許多JVM使用的垃圾回收算法。

2)Conservative garbage collection(保守)

哪一種算法最好?答案是沒有最好。

EMM作爲很常用的垃圾回收算法,有5種基本方法:
  1)Table-driven algorithms
  表驅動算法把內存分爲固定尺寸的塊集合。這些塊使用抽象數據結構進行索引。比如一個bit對應一個塊,用0和1表示是否分配。不利因素:位映射依賴於內存塊的尺寸;另外,搜索一系列的空閒內存塊可能需要搜索整個bit映射表,這影響性能。

  2)Sequential fit
順序適應算法允許內存分爲不同的尺寸。此算法跟蹤已分配和空閒的Heap,標記空閒塊的起始地址和結束地址。它有三種子分類:
  (1) First fit(首次適應)——分配找到的第一個適合內存請求的塊
  (2) Best fit(最佳適應)——分配最適合內存請求的塊
  (3) Worst fit(最不適應)——分配最大的塊給內存請求

3)Buddy systems
  Buddy systems算法的主要目的是加速已分配內存在釋放後的合併速度。顯示內存管理EMM使用Buddy systems算法可能導致內部碎片。

4)Segregated storage
  隔離存儲技術涉及到把Heap分成多個區域(zone),併爲每個區域採用不同的內存管理計劃。這是很有效的方法。

5)Sub-allocators
  子配置技術嘗試解決在Run-time System下分配大塊內存並單獨管理的內存分配問題。換句話說,程序完全負責自己的私有存儲堆(stockpile)的內存分配和回收,無需run-time System的幫助。它可能帶來額外的複雜性,但是你可以顯著地提高性能。在1990年的《C Compiler Design》一書中,Allen Holub就極好地利用了Sub-allocators來加速其編譯器的實現。

  注意,顯示內存管理EMM必須是靈活的,能夠響應數種不同類型的請求。

  最後,使用EMM還是使用AMM?這是一個Religious question,憑個人喜好。EMM在複雜的開銷下實現了速度和控制。AMM犧牲了性能,但換來了簡單性。

  無論是emm,amm分配內存,出現了oom的問題,由於加載內存過大也是在所難免。

  四。優化Dalvik虛擬機的堆內存分配

  對於Android平臺來說,其託管層使用的Dalvik Java VM從目前的表現來看還有很多地方可以優化處理,比如我們在開發一些大型遊戲或耗資源的應用中可能考慮手動干涉GC處理,使用dalvik.system.VMRuntime類提供的setTargetHeapUtilization方法可以增強程序堆內存的處理效率。當然具體原理我們可以參考開源工程,這裏我們僅說下使用方法:private final static float TARGET_HEAP_UTILIZATION = 0.75f;在程序onCreate時就可以調用VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);即可。

       Android堆內存也可自己定義大小

       對於一些Android項目,影響性能瓶頸的主要是Android自己內存管理機制問題,目前手機廠商對RAM都比較吝嗇,對於軟件的流暢性來說RAM對性能的影響十分敏感,除了 優化Dalvik虛擬機的堆內存分配外,我們還可以強制定義自己軟件的堆內存大小,我們使用Dalvik提供的 dalvik.system.VMRuntime類來設置最小堆內存爲例:

       private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;

       VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE); //設置最小heap內存爲6MB大小。當然對於內存吃緊來說還可以通過手動干涉GC去處理

  注意了,這個設置dalvik虛擬機的配置的方法對Android4.0 設置無效。

  這就是我對Android的oom的一點看法.

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