Android高級性能調優;難以想象的OOM!

前言;

本文發現了一類OOM(OutOfMemoryError),這類OOM的特色是崩潰時java堆內存和設備物理內存都充足,下文將帶你探索並解釋這類OOM拋出的緣由。java

文末有demo地址。linux

關鍵詞:git

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

1、引子

對於每個移動開發者,內存是都須要當心使用的資源,而線上出現的 OOM(OutOfMemoryError)都會讓開發者抓狂,由於咱們一般仰仗的直觀的堆棧信息對於定位這種問題一般幫助不大。網上有不少資料教咱們如何「緊衣縮食「的利用寶貴的堆內存(好比,使用小圖片,bitmap 複用等),但是:markdown

1.線上的 OOM 真的全是因爲堆內存緊張致使的嗎?網絡

2.有沒有 App 堆內存寬裕,設備物理內存也寬裕的狀況下發生 OOM 的可能?多線程

內存充裕的時候出現 OOM 崩潰?app

3.看似難以想象,然而,最近筆者在調查一個問題的時候,經過自研的 APM 平臺發現公司的一個產品的大部分 OOM 確實有這樣的特徵,即:OOM 崩潰時,java 堆內存遠遠低於 Android 虛擬機設定的上限,而且物理內存充足,SD 卡空間充足socket

既然內存充足,這時候爲何會有 OOM 崩潰呢?ionic

2、問題描述

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

什麼致使了 OOM 的產生?

下面是幾個關於 Android 官方聲明內存限制閾值的 API:


一般認爲 OOM 發生是因爲 java 堆內存不夠用了,即;


這種 OOM 能夠很是方便的驗證(好比: 經過 new byte[] 的方式嘗試申請超過閾值maxMemory() 的堆內存),一般這種 OOM 的錯誤信息一般以下:


而前面已經提到了,本文中發現的 OOM 案例中堆內存充裕(Runtime.getRuntime().maxMemory() 大小的堆內存還剩餘很大一部分),設備當前內存也很充裕(ActivityManager.MemoryInfo.availMem 還有不少)。這些 OOM 的錯誤信息大體有下面兩種:

1 . 這種 OOM 在 Android6.0,Android7.0 上各個機型均有發生,文中簡稱爲 OOM ,錯誤信息以下:


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


3、問題分析及解決

3.1代碼分析

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

1. Android 虛擬機最終拋出OutOfMemoryError 的代碼位於/art/runtime/thread.cc


2. 搜索代碼能夠發現如下幾個地方調用了上述方法拋出 OutOfMemoryError 錯誤

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


這種拋出的其實就是堆內存不夠用的時候,即前面提到的申請堆內存大小超過了Runtime.getRuntime().maxMemory()

1 . 第二個地方是建立線程時


對比錯誤信息,能夠知道咱們遇到的 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 系統對對應進程的限制,下面是一個樣例:


用排除法篩選上面樣例中的 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 的產生。

錯誤信息及堆棧以下:


能夠看出,此 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 時錯誤信息以下:


能夠看出 錯誤信息與咱們線上遇到的 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 具備以下信息:


線上驗收及問題解決

本地嘗試復現的 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 是怎麼發生的,首先線程建立的簡易版流程圖以下所示:


上圖中,線程建立大概有兩個關鍵的步驟:

  • 第一列中的 建立線程私有的結構體 JNIENV(JNI 執行環境,用於 C 層調用 Java 層代碼)
  • 第二列中的 調用 posix C 庫的函數 pthread_create 進行線程建立工做

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

1. 圖中節點①,/art/runtime/thread.cc 中的函數Thread:CreateNativeThread部分節選代碼以下:


可知:

  • 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。


2. 圖中節點②和③是建立 JNIENV 過程的關鍵節點,節點②/art/runtime/mem_map.cc 中 函數 MemMap:MapAnonymous 的做用是爲 JNIENV 結構體中Indirect_Reference_table(C 層用於存儲 JNI 局部 / 全局變量)申請內存,申請內存的方法是節點③所示的函數ashmem_create_region(建立一塊 ashmen 匿名共享內存, 並返回一個文件描述符)。節點②代碼節選以下:


咱們線上的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 的方式,節點⑤代碼節選以下:


打印的錯誤信息與圖 [3-7] 中進程邏輯地址佔滿致使的 OOM 錯誤信息吻合,圖 [3-7] 中錯誤信息" Try again"說明系統全局錯誤標識 errno 爲 11(見圖 [3-10] 系統錯誤定義_errdefs.h). pthread_create 過程當中,節點4相關代碼以下:


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

4、結論及監控

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來監控線程使用狀況。

5、Demo


六,難以想象的OOM,Android高級腦圖,全套視頻

1.難以想象的OOM;



2.Android高級腦圖;


3.Android高級視頻;



全套高級視頻尚在整理完善,免費分享,歡迎關注謝謝
相關文章
相關標籤/搜索