Android高階效能調優;不可思議的OOM!

Android架構技術分享發表於2019-04-23

前言;

本文發現了一類OOM(OutOfMemoryError),這類OOM的特點是崩潰時java堆記憶體和裝置實體記憶體都充足,下文將帶你探索並解釋這類OOM丟擲的原因。

文末有demo地址。

關鍵詞:

OutOfMemoryError, OOM,pthread_create failede,Could not allocate JNI Env

一、引子

對於每一個移動開發者,記憶體是都需要小心使用的資源,而線上出現的 OOM(OutOfMemoryError)都會讓開發者抓狂,因為我們通常仰仗的直觀的堆疊資訊對於定位這種問題通常幫助不大。網上有很多資料教我們如何“緊衣縮食“的利用寶貴的堆記憶體(比如,使用小圖片,bitmap 複用等),可是:

1.線上的 OOM 真的全是由於堆記憶體緊張導致的嗎?

2.有沒有 App 堆記憶體寬裕,裝置實體記憶體也寬裕的情況下發生 OOM 的可能?

記憶體充裕的時候出現 OOM 崩潰?

3.看似不可思議,然而,最近筆者在調查一個問題的時候,通過自研的 APM 平臺發現公司的一個產品的大部分 OOM 確實有這樣的特徵,即:OOM 崩潰時,java 堆記憶體遠遠低於 Android 虛擬機器設定的上限,並且實體記憶體充足,SD 卡空間充足

既然記憶體充足,這時候為什麼會有 OOM 崩潰呢?

二、問題描述

在詳細描述問題之前,先弄清楚一個問題:

什麼導致了 OOM 的產生?

下面是幾個關於 Android 官方宣告記憶體限制閾值的 API:

Android高階效能調優;不可思議的OOM!

通常認為 OOM 發生是由於 java 堆記憶體不夠用了,即;

Android高階效能調優;不可思議的OOM!

這種 OOM 可以非常方便的驗證(比如: 通過 new byte[] 的方式嘗試申請超過閾值maxMemory() 的堆記憶體),通常這種 OOM 的錯誤資訊通常如下:

Android高階效能調優;不可思議的OOM!

而前面已經提到了,本文中發現的 OOM 案例中堆記憶體充裕(Runtime.getRuntime().maxMemory() 大小的堆記憶體還剩餘很大一部分),裝置當前記憶體也很充裕(ActivityManager.MemoryInfo.availMem 還有很多)。這些 OOM 的錯誤資訊大致有下面兩種:

1 . 這種 OOM 在 Android6.0,Android7.0 上各個機型均有發生,文中簡稱為 OOM ,錯誤資訊如下:

Android高階效能調優;不可思議的OOM!

2 . 集中發生在 Android7.0 及以上的華為手機(EmotionUI_5.0 及以上)的 OOM,簡稱為 OOM 二,對應錯誤資訊如下:

Android高階效能調優;不可思議的OOM!

三、問題分析及解決

3.1程式碼分析

Android 系統中,OutOfMemoryError 這個錯誤是怎麼被系統丟擲的?下面基於 Android6.0 的程式碼進行簡單分析:

1. Android 虛擬機器最終丟擲OutOfMemoryError 的程式碼位於/art/runtime/thread.cc

Android高階效能調優;不可思議的OOM!

2. 搜尋程式碼可以發現以下幾個地方呼叫了上述方法丟擲 OutOfMemoryError 錯誤

3. 第一個地方是堆操作時

Android高階效能調優;不可思議的OOM!

這種丟擲的其實就是堆記憶體不夠用的時候,即前面提到的申請堆記憶體大小超過了Runtime.getRuntime().maxMemory()

1 . 第二個地方是建立執行緒時

Android高階效能調優;不可思議的OOM!

對比錯誤資訊,可以知道我們遇到的 OOM 崩潰就是這個時機,即建立執行緒的時候(Thread::CreateNativeThread)產生的。

2 . 還有其他的一些錯誤資訊如“[XXXClassName] of length XXX would overflow”是系統限制String/Array 的長度所致,不在本文討論之列。

那麼,我們關心的就是Thread::CreateNativeThread 時丟擲的 OOM 錯誤,建立執行緒為什麼會導致 OOM 呢?

3.2推斷

既然丟擲來 OOM,一定是執行緒建立過程中觸發了某些我們不知道的限制,既然不是 Art 虛擬機器為我們設定的堆上限,那麼可能是更底層的限制。Android 系統基於 linux,所以 linux 的限制對於 Android 同樣適用,這些限制有:

1 ./proc/pid/limits 描述著 linux 系統對對應程式的限制,下面是一個樣例:

Android高階效能調優;不可思議的OOM!

用排除法篩選上面樣例中的 limits:

  • Max stack size,Max processes 的限制是整個系統的,不是針對某個程式的,排除;
  • Max locked memory ,排除,後面會分析,執行緒建立過程中分配執行緒私有 stack 使用的 mmap 呼叫沒有設定 MAP_LOCKED,所以這個限制與執行緒建立過程無關 ;
  • Max pending signals,c 層訊號個數閾值,無關,排除 ;
  • Max msgqueue size,Android IPC 機制不支援訊息佇列,排除。

剩下的 limits 項中,Max open files 這一項限制最可疑Max open files 表示 每個程式最大開啟檔案的數目,程式 每開啟一個檔案就會產生一個檔案描述符 fd(記錄在 /proc/pid/fd 下面),這個限制表明 fd 的數目不能超過 Max open files 規定的數目。

後面分析執行緒建立過程中會發現過程中涉有及到檔案描述符。

2 . /proc/sys/kernel 中描述的限制

這些限制中與執行緒相關的是 /proc/sys/kernel/threads-max,規定了每個程式建立執行緒數目的上限,所以執行緒建立導致 OOM 的原因也有可能與這個限制相關。

3.3驗證

下面對上述的推斷進行驗證,分兩步:本地驗證和線上驗收。

  • 本地驗證:在本地驗證推斷,試圖復現與圖 [2-4]OOM 一與圖 [2-5]OOM 二所示錯誤訊息一致的 OOM
  • 線上驗收:下發外掛,驗收線上使用者 OOM 時確實是由於上面的推斷的原因導致的

本地驗證

實驗一: 觸發大量網路連線(每個連線處於獨立的執行緒中)並保持,每開啟一個 socket 都會增加一個 fd(/proc/pid/fd 下多一項)

注:不只有這一種增加 fd 數的方式,也可以用其他方法,比如開啟檔案,建立 handlerthread 等等

  • 實驗預期:當程式 fd 數(可以通過 ls /proc/pid/fd | wc -l 獲得)突破 /proc/pid/limits 中規定的 Max open files 時,產生 OOM;
  • 實驗結果:當 fd 數目到達 /proc/pid/limits 中規定的 Max open files 時,繼續開執行緒確實會導致 OOM 的產生。

錯誤資訊及堆疊如下:

Android高階效能調優;不可思議的OOM!

可以看出,此 OOM 發生時的錯誤資訊確與線上發現的 OOM 一的“Could not allocate JNI Env” 吻合,因此線上上報的 OOM 一 可能 就是由 FD 數超限導致的,不過最終確定需要到線上進行驗證 (下一小節)。此外從 ART 虛擬機器的 Log 中看出,還有一個關鍵的資訊 “ art: ashmem_create_region failed for 'indirect ref table': Too many open files”,後面會用於問題定位及解釋。

實驗二:建立大量的空執行緒(不做任何事情,直接 sleep)

  • 實驗預期:
  • 當執行緒數(可以在/proc/pid/status 中的threads項實時檢視)超過/proc/sys/kernel/threads-max 中規定的上限時產生 OOM 崩潰。
  • 實驗結果:
  • 在 Android7.0 及以上的華為手機(EmotionUI_5.0 及以上)的手機產生 OOM,這些手機的執行緒數限制都很小 (應該是華為 rom 特意修改的 limits),每個程式只允許最大同時開 500 個執行緒,因此很容易復現了。

OOM 時錯誤資訊如下:

Android高階效能調優;不可思議的OOM!

可以看出 錯誤資訊與我們線上遇到的 OOM 二吻合:"pthread_create (1040KB stack) failed: Out of memory" 另外 ART 虛擬機器還有一個關鍵 Log:“pthread_create failed: clone failed: Out of memory”,後面會用於問題定位及解釋。

1 . 其他 Rom 的手機執行緒數的上限都比較大,不容易復現上述問題。但是,對於 32 位的系統,當程式的邏輯地址空間不夠的時候也會產生 OOM,每個執行緒通常需要 mapp 1MB 左右的 stack 空間(stack 大小可以自行設定),32 為系統程式邏輯地址 4GB,使用者空間少於 3GB。邏輯地址空間不夠(已用邏輯空間地址可以檢視 /proc/pid/status 中的 VmPeak/VmSize 記錄),此時建立執行緒產生的 OOM 具有如下資訊:

Android高階效能調優;不可思議的OOM!

線上驗收及問題解決

本地嘗試復現的 OOM 錯誤資訊中圖 [3-5] 與線上 OOM 一情況比較吻合,圖 [3-6] 與線上 OOM 二的情況比較吻合,但線上的 OOM 一真的時 FD 數目超限,OOM 二真的是由於華為手機執行緒數超限的原因導致的嗎?最終確定還需要取線上裝置的資料進行驗證。

驗證方法:

下發外掛到線上使用者,當 Thread.UncaughtExceptionHandler 捕獲到OutOfMemoryError 時記錄 /proc/pid 目錄下的如下資訊:

1. /proc/pid/fd 目錄下檔案數 (fd 數)

2. /proc/pid/status 中 threads 項(當前執行緒數目)

3. OOM 的日誌資訊(出了堆疊資訊還包含其他的一些 warning 資訊

線上 OOM 一驗證

發生 OOM 一的線上裝置中採集到的資訊:

1. /proc/pid/fd 目錄下檔案數與 /proc/pid/limits 中的 Max open files 數目持平,證明 FD 數目已經滿了;

2. 崩潰時日誌資訊與圖 [3-5] 基本一致;

由此,證明 線上的 OOM 一確實是由於 FD 數目過多導致的 OOM,推斷驗證成功。

OOM 一的定位與解決:

最終原因是 App 中使用的長連線庫再某些時候會有瞬時發出大量 http 請求的 bug(導致 FD 數激增),已修復。

線上 OOM 二驗證 集中在華為系統的 OOM 二崩潰時收集到的資訊樣例如下,(收集的樣例中包含的 devicemodel 有 VKY-AL00,TRT-AL00A,BLN-AL20,BLN-AL10,DLI-AL10,TRT-TL10,WAS-AL00 等):

1. /proc/pid/status 中 threads 記錄全部到達上限:Threads: 500;

2. 崩潰時日誌資訊與圖 [3-6] 基本一致;

推斷驗證成功,即 執行緒數受限導致建立執行緒時 clone failed 導致了線上的 OOM 二

OOM 二的定位與解決:

關於 App 業務程式碼中的問題還在定位修復中。

3.4解釋

下面從程式碼分析本文描述的 OOM 是怎麼發生的,首先執行緒建立的簡易版流程圖如下所示:

Android高階效能調優;不可思議的OOM!

上圖中,執行緒建立大概有兩個關鍵的步驟:

  • 第一列中的 建立執行緒私有的結構體 JNIENV(JNI 執行環境,用於 C 層呼叫 Java 層程式碼)
  • 第二列中的 呼叫 posix C 庫的函式 pthread_create 進行執行緒建立工作

下面對流程圖中關鍵節點(圖中有標號的)進行說明:

1. 圖中節點①,/art/runtime/thread.cc 中的函式Thread:CreateNativeThread部分節選程式碼如下:

Android高階效能調優;不可思議的OOM!

可知:

  • JNIENV 建立不成功時產生 OOM 的錯誤資訊為 "Could not allocate JNI Env",與文中 OOM 一一致

pthread_create失敗時丟擲 OOM 的錯誤資訊為"pthread_create (%s stack) failed: %s".其中詳細的錯誤資訊由 pthread_create 的返回值(錯誤碼)給出。錯誤碼與錯誤描述的對應關係可以參見 bionic/libc/include/sys/_errdefs.h中的定義。文中 OOM 二的具體錯誤資訊為"Out of memory",就說明 pthread_create 的返回值為 12。

Android高階效能調優;不可思議的OOM!

2. 圖中節點②和③是建立 JNIENV 過程的關鍵節點,節點②/art/runtime/mem_map.cc 中 函式 MemMap:MapAnonymous 的作用是為 JNIENV 結構體中Indirect_Reference_table(C 層用於儲存 JNI 區域性 / 全域性變數)申請記憶體,申請記憶體的方法是節點③所示的函式ashmem_create_region(建立一塊 ashmen 匿名共享記憶體, 並返回一個檔案描述符)。節點②程式碼節選如下:

Android高階效能調優;不可思議的OOM!

我們線上的OOM 一的錯誤資訊"ashmem_create_region failed for 'indirect ref table': Too many open files",與此處列印的資訊吻合。"Too many open files"的錯誤描述說明此處的 errno(系統全域性錯誤標識)為 24(見圖 [3-10] 系統錯誤定義 _errdefs.h)。由此看出我們線上的 OOM 一是由於檔案描述符數目已滿,ashmem_create_region 無法返回新的 FD 而導致的

3. 圖中節點④和⑤是呼叫 C 庫建立執行緒時的環節,建立執行緒首先 呼叫 __allocate_thread 函式申請執行緒私有的棧記憶體 (stack) 等,然後 呼叫 clone 方法進行執行緒建立.申請 stack 採用的時 mmap 的方式,節點⑤程式碼節選如下:

Android高階效能調優;不可思議的OOM!

列印的錯誤資訊與圖 [3-7] 中程式邏輯地址佔滿導致的 OOM 錯誤資訊吻合,圖 [3-7] 中錯誤資訊" Try again"說明系統全域性錯誤標識 errno 為 11(見圖 [3-10] 系統錯誤定義_errdefs.h). pthread_create 過程中,節點4相關程式碼如下:

Android高階效能調優;不可思議的OOM!

此處輸出的錯誤日誌"pthread_create failed: clone failed: %s"與我們線上發現的 OOM 二吻合,圖 [3-6] 中的錯誤描述" Out of memory"說明系統全域性錯誤標識 errno 為 12(見圖 [3-10] 系統錯誤定義 _errdefs.h)。 由此線上的 OOM 二就是由於執行緒數的限制而在節點 5 clone 失敗導致 OOM。

四、結論及監控

4.1導致OOM發生的原因

綜上,可以導致 OOM 的原因有以下幾種:

1. 檔案描述符 (fd) 數目超限,即 proc/pid/fd 下檔案數目突破 /proc/pid/limits 中的限制。可能的發生場景有:短時間內大量請求導致 socket 的 fd 數激增,大量(重複)開啟檔案等 ;

2. 執行緒數超限,即proc/pid/status中記錄的執行緒數(threads 項)突破 /proc/sys/kernel/threads-max 中規定的最大執行緒數。可能的發生場景有:app 內多執行緒使用不合理,如多個不共享執行緒池的 OKhttpclient 等等 ;

3. 傳統的 java 堆記憶體超限,即申請堆記憶體大小超過了Runtime.getRuntime().maxMemory();

4. (低概率)32 為系統程式邏輯空間被佔滿導致 OOM;

5. 其他。

4.2監控措施

可以利用 linux 的 inotify 機制進行監控:

  • watch /proc/pid/fd來監控 app 開啟檔案的情況,
  • watch /proc/pid/task來監控執行緒使用情況。

五、Demo


六,不可思議的OOM,Android高階腦圖,全套視訊

1.不可思議的OOM;

Android高階效能調優;不可思議的OOM!


2.Android高階腦圖;

Android高階效能調優;不可思議的OOM!

3.Android高階視訊;

Android高階效能調優;不可思議的OOM!


全套高階視訊尚在整理完善,免費分享,歡迎關注謝謝


相關文章