分析幾種預防OOM的方法
一、OOM介紹
1、VM執行時記憶體分析
JVM執行Java程式的過程中,會使用到各種資料區域,這些區域有各自的用途、建立和銷燬時間。JVM包括下列幾個執行時資料區域:
-
1.程式計數器(Program Counter Register):
此記憶體區域是唯一一個在VM Spec中沒有規定任何OutOfMemoryError情況的區域。
-
2.Java虛擬機器棧(Java Virtual Machine Stacks):
在VM Spec中對這個區域規定了兩種異常狀況:如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常;如果VM棧可以動態擴充套件(VM Spec中允許固定長度的VM棧),當擴充套件時無法申請到足夠記憶體則丟擲OutOfMemoryError異常。
-
3.本地方法棧(Native Method Stacks):
和VM棧一樣,這個區域也會丟擲StackOverflowError和OutOfMemoryError異常。
-
4.Java堆(Java Heap)
對於絕大多數應用來說,Java堆是虛擬機器管理最大的一塊記憶體。Java堆是被所有執行緒共享的,在虛擬機器啟動時建立。Java堆的唯一目的就是存放物件例項(以及陣列),絕大部分的物件例項都在這裡分配。
Java堆內還有更細緻的劃分:新生代、老年代,再細緻一點的:eden、from survivor、to survivor,甚至更細粒度的本地執行緒分配緩衝(TLAB)等。
根據VM Spec的要求,Java堆可以處於物理上不連續的記憶體空間,它邏輯上是連續的即可,就像我們的磁碟空間一樣。實現時可以選擇實現成固定大小的,也可以是可擴充套件的,不過當前所有商業的虛擬機器都是按照可擴充套件來實現的(通過-Xmx和-Xms控制)。如果在堆中無法分配記憶體,並且堆也無法再擴充套件時,將會丟擲OutOfMemoryError異常。
-
5.方法區(Method Area)
-
6.執行時常量池(Runtime Constant Pool):
執行時常量池是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法在申請到記憶體時會丟擲OutOfMemoryError異常。
2、OOM出現原因
-
已知前提
:
Android的應用程式所能申請的最大記憶體都是有限的 -
定義
:
OOM是指APP向系統申請記憶體的請求超過了應用所能有的最大閥值的記憶體,系統無法再分配多餘的空間,就會造成OOM -
出現原因
:
1、持續發生了記憶體洩漏(Memory Leak),累積到一定程度導致OOM
持續發生了記憶體洩漏(Memory Leak),累積到一定程度導致OOM;
2、一次性申請很多記憶體(比如說一次建立大的陣列或者是載入大的檔案如圖片的時候) -
造成結果
:
發生oom錯誤,程序被強制kill掉,kill掉的程序記憶體會被系統回收
二、StrictMode介紹
StrictMode:嚴苛模式,主要用來檢測程式中幾種違例情況的Android原生開發者工具。 主要分為ThreadPolicy(執行緒檢測策略)跟VmPolicy(虛擬機器檢測策略)兩類檢測內容:
1、ThreadPolicy
- detectDiskReads()--主執行緒讀取檔案
- detectDiskWrites()--主執行緒寫檔案
- detectNetwork()--主執行緒網路操作
- detectCustomSlowCalls()--自定義耗時操作
2、VmPolicy
- detectLeakedSqlLiteObjects--Sqlite物件洩漏
- detectActivityLeaks--Activity洩漏
- detectLeakedClosableObjects--未關閉的Closable物件洩露
- detectLeakedRegistrationObjects--廣播註冊後沒取消註冊
3、整合方式
可以按照不同場景需求選擇檢測不同違規操作。推薦在Application onCreate()方法中整合:
if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()//執行緒策略(ThreadPolicy) .detectDiskReads()//檢測在UI執行緒讀磁碟操作 .detectDiskWrites()//檢測UI執行緒寫磁碟操作 .detectCustomSlowCalls()//發現UI執行緒呼叫的哪些方法執行得比較慢 .detectResourceMismatches()//最低版本為API23發現資源不匹配 .detectNetwork() //檢測在UI執行緒執行網路操作 .penaltyDialog()//一旦檢測到彈出Dialog .penaltyDropBox()//一旦檢測到將資訊存到DropBox資料夾中 data/system/dropbox .penaltyLog()//一旦檢測到將資訊以LogCat的形式打印出來 .permitDiskReads()//允許UI執行緒在磁碟上讀操作 .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()//虛擬機器策略(VmPolicy) .detectActivityLeaks()//最低版本API11 使用者檢查 Activity 的記憶體洩露情況 .detectCleartextNetwork()//最低版本為API23檢測明文的網路 .detectFileUriExposure()//最低版本為API18檢測file://或者是content:// .detectLeakedClosableObjects()//最低版本API11資源沒有正確關閉時觸發 .detectLeakedRegistrationObjects()//最低版本API16BroadcastReceiver、ServiceConnection是否被釋放 .detectLeakedSqlLiteObjects()//最低版本API9資源沒有正確關閉時回觸發 .setClassInstanceLimit(TestLeakActivity.class, 2)//設定某個類的同時處於記憶體中的例項上限,可以協助檢查記憶體洩露 .penaltyLog() .build()); }
4、整合StrictMode後代碼違規表現
由上面StrictMode API可知,檢測到違規程式碼時,我們可以選擇以應用崩潰/彈框/日誌輸出等形式表現出來。一般推薦輸出到日誌即可。下面簡單列舉下執行到程式碼中違規操作時日誌表現:
- I/O流未關閉
2019-05-06 19:51:25.523 22941-22951/com.whh.strictmode E/StrictMode: A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks. java.lang.Throwable: Explicit termination method 'close' not called at dalvik.system.CloseGuard.open(CloseGuard.java:180) at java.io.FileOutputStream.<init>(FileOutputStream.java:222) at java.io.FileOutputStream.<init>(FileOutputStream.java:169) at com.whh.strictmode.MainActivity.testReadWrite(MainActivity.java:83) at com.whh.strictmode.MainActivity.onClick(MainActivity.java:53)
- Activity洩漏
2019-05-06 19:52:48.467 23148-23148/com.whh.strictmode E/WindowManager: android.view.WindowLeaked: Activity com.whh.strictmode.leakactivity.TestLeakActivity has leaked window DecorView@86a60b[] that was originally added here at android.view.ViewRootImpl.<init>(ViewRootImpl.java:418) at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:331) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93) at android.app.Dialog.show(Dialog.java:322) at com.whh.strictmode.leakactivity.TestLeakActivity.onCreate(TestLeakActivity.java:38) 2019-05-06 19:53:18.916 23148-23148/com.whh.strictmode E/StrictMode: class com.whh.strictmode.leakactivity.TestLeakActivity; instances=2; limit=1 android.os.StrictMode$InstanceCountViolation: class com.whh.strictmode.leakactivity.TestLeakActivity; instances=2; limit=1 at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
- Bitmap洩漏
Caused by: java.lang.OutOfMemoryError: Failed to allocate a 51916812 byte allocation with 4188384 free bytes and 24MB until OOM at dalvik.system.VMRuntime.newNonMovableArray(Native Method) at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method) at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:620) at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:660) at com.whh.strictmode.MainActivity.testBitmap(MainActivity.java:105) at com.whh.strictmode.MainActivity.onClick(MainActivity.java:53)
三、如何避免OOM
1、I/O流用完及時關閉
推薦用try-with-resource語句--更優雅的關閉資源。
(JDK7及其之後的資源關閉方式)
try-with-resource並不是JVM虛擬機器的新增功能,只是JDK實現了一個語法糖,實際實現原理還是try-catch-finally。
以前用try-catch-finally使用I/O流程式碼如下:
//try-catch外部定義I/O流 InputStream is = null; OutputStream os = null; try { //try括號中I/O賦值 is = new FileInputStream(src); os = new FileOutputStream(dst); //...進行一些I/O操作 } catch (Exception e) { } finally { //finally中關閉I/O流 try { if (is != null) { is.close(); } } catch (IOException e) { } try { if (os != null) { os.close(); } } catch (IOException e) { } }
這種方式程式碼繁雜不說,有時候還容易忘記關閉輸入輸出流。而換成try-with-resource語句後,程式碼如下:
try (InputStream is = new FileInputStream(src); OutputStream os = new FileOutputStream(dst)) { //...進行一些I/O操作 } catch (Exception e) { }
這種方式優雅簡潔,而且不用擔心忘記關閉輸入輸出流,因為try()括號中的I/O流會自動關閉
2、確保Activity例項引用不會被其他類長期持有
-
1、Activity中非靜態內部類會持有Activity例項引用:Java的非靜態內部類在構造的時候,會將外部類的引用傳遞進來,並且作為內部類的一個屬性,因此,內部類會隱式地持有其外部類的引用。
所以,定義內部類時一定要注意看當前類是否持有Activity引用或者當前類是否是Activity 。 -
2、將Activity當成Context傳入其他類,會導致其他類持有Activity例項引用。
所以,一般情況下要用Context時最好用ApplicationContext,必須用ActivityContext時要注意Context引用回收。 -
3、將Activity中View注入其他類時,由於View持有ActivityContext引用,也會導致其他類持有Activity例項引用。
所以,將View注入其他類時,確保該View引用在用完後被回收 。 -
4、Activity實現了某些介面,作為觀察者被註冊到其他類時,也會導致Activity引用被其他類持有。
所以,Activity中註冊監聽與反註冊最好成對出現 。 - 5、Activity生命週期內註冊廣播時,要在對應生命週期中取消廣播註冊。
3、圖片載入
- 1、儘量使用圖片載入框架(ImageLoader/Picasso/Glide...)載入圖片,因為使用圖片載入框架不需要去考慮圖片Bitmap回收問題等,而且0圖片載入框架基本都支援擴充套件(比如圖片轉換等)跟自定義(比如圖片快取大小、位置等)。
- 2、不用框架載入圖片時,請用LruCache管理圖片快取列表。
- 3、如果以上兩種方式都不用,一定要確保Bitmap使用完後合理回收。
- 4、載入圖片時,一定要考慮到圖片尺寸壓縮/顏色質量,根據需要保證圖片快取的合理大小跟顏色質量。由於Android載入圖片基本使用BitmapFactory載入,這裡簡單介紹下兩種圖片利用技巧:
//1、利用inSampleSize進行圖片取樣 BitmapFactory.Options options = new BitmapFactory.Options(); //inSampleSize越大,壓縮率越大。inSampleSize==1時表示原圖 //當確定圖片大小跟需要的圖片大小時,可以計算出取樣率進行圖片壓縮 options.inSampleSize = 2; Bitmap bmp = BitmapFactory.decodeFile(path, options);
//2、利用inSampleSize進行圖片取樣 BitmapFactory.Options options = new BitmapFactory.Options(); //當對圖片透明度需求不高時,可以將圖片顏色質量設為RGB_565 options.inPreferredConfig = Config.RGB_565; Bitmap bmp = BitmapFactory.decodeFile(path, options);
//3、利用inBitmap複用舊的Bitmap的記憶體,而不用重新分配以及銷燬舊Bitmap,進而改善執行效率 BitmapFactory.Options options = new BitmapFactory.Options(); //mOldBitmap是一箇舊的Bitmap,必須是mutable的(支援修改).新的bmp直接複用該mOldBitmap的記憶體空間 //使用條件是mOldBitmap佔用的空間必須大於等於新bmp佔用的記憶體空間 options.inBitmap = mOldBitmap; Bitmap bmp = BitmapFactory.decodeFile(path, options);