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 issuesCosts 複雜、記賬、記憶體泄露、指標懸空 不錯的效能—————————————————————————————————————— 早期的記憶體回收行程非常慢,往往佔用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 設定無效。