天天看點

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中對象有強可及對象、軟可及對象、弱可及對象、虛可及對象和不可到達對象。應用的強弱順序是強、軟、弱、和虛。對于對象是屬于哪種可及的對象,由他的最強的引用決定。如下:

 

虛引用

   就是沒有的意思,建立虛引用之後通過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中配置相應的圖檔資源, 否則在不同分辨率機器上都是同樣大小(像素點數量),顯示出來的大小就不對了。

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

  以下奉上一個方法,以最省記憶體的方式讀取本地資源的圖檔

昨天在模拟器上給gallery放入圖檔的時候,出現java.lang.outofmemoryerror: bitmap size exceeds vm budget 異常,圖像大小超過了ram記憶體。 

      模拟器ram比較小,隻有8m記憶體,當我放入的大量的圖檔(每個100多k左右),就出現上面的原因。 

由于每張圖檔先前是壓縮的情況,放入到bitmap的時候,大小會變大,導緻超出ram記憶體,具體解決辦法如下: 

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

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

優化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取圖檔) 

以上代碼可以優化記憶體溢出,但它隻是改變圖檔大小,并不能徹底解決記憶體溢出。

● eg2:(通過路徑去圖檔)

  這樣,能夠壓縮足夠的比例,但是對于小記憶體手機,特别是那種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的一點看法.