您好,登錄后才能下訂單哦!
本文發現了一類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:
圖 2-21
通常認為 OOM 發生是由于 java 堆內存不夠用了,即
圖 2-2 Java 堆 OOM 產生原因
這種 OOM 可以非常方便的驗證(比如: 通過 new byte[] 的方式嘗試申請超過閾值maxMemory() 的堆內存),通常這種 OOM 的錯誤信息通常如下:
圖 2-3 堆內存不夠導致的 OOM 的錯誤信息
而前面已經提到了,本文中發現的 OOM 案例中堆內存充裕(Runtime.getRuntime().maxMemory() 大小的堆內存還剩余很大一部分),設備當前內存也很充裕(ActivityManager.MemoryInfo.availMem 還有很多)。這些 OOM 的錯誤信息大致有下面兩種:
1 . 這種 OOM 在 Android6.0,Android7.0 上各個機型均有發生,文中簡稱為 OOM?一,錯誤信息如下:
圖 2-4 OOM 一的錯誤信息
2 . 集中發生在 Android7.0 及以上的華為手機(EmotionUI_5.0 及以上)的 OOM,簡稱為?OOM 二,對應錯誤信息如下:
(圖 2-5 OOM 二的錯誤信息)
3.1代碼分析
Android 系統中,OutOfMemoryError 這個錯誤是怎么被系統拋出的?下面基于 Android6.0 的代碼進行簡單分析:
1. Android 虛擬機最終拋出OutOfMemoryError 的代碼位于/art/runtime/thread.cc
(圖 3-1 ART Runtime 拋出的位置)
2. 搜索代碼可以發現以下幾個地方調用了上述方法拋出 OutOfMemoryError 錯誤
3. 第一個地方是堆操作時
圖 3-2 Java 堆 OOM
這種拋出的其實就是堆內存不夠用的時候,即前面提到的申請堆內存大小超過了Runtime.getRuntime().maxMemory()
1 . 第二個地方是創建線程時
圖 3-3 線程創建時 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 系統對對應進程的限制,下面是一個樣例:
(圖 3-4 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 的產生。
錯誤信息及堆棧如下:
(圖 3-5 FD 數超限導致 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 時錯誤信息如下:
(圖 3-6 線程數超限導致的 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 具有如下信息:
(圖 3-7 邏輯地址空間占滿導致的 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 是怎么發生的,首先線程創建的簡易版流程圖如下所示:
(圖 3-8 線程創建流程)
上圖中,線程創建大概有兩個關鍵的步驟:
第一列中的?創建線程私有的結構體 JNIENV(JNI 執行環境,用于 C 層調用 Java 層代碼)
第二列中的?調用 posix C 庫的函數 pthread_create 進行線程創建工作
下面對流程圖中關鍵節點(圖中有標號的)進行說明:
1. 圖中節點①,/art/runtime/thread.cc 中的函數Thread:CreateNativeThread部分節選代碼如下:
(圖 3-9 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。
圖 3-10 系統錯誤定義 _errdefs.h
2. 圖中節點②和③是創建 JNIENV 過程的關鍵節點,節點②/art/runtime/mem_map.cc 中 函數 MemMap:MapAnonymous 的作用是為 JNIENV 結構體中Indirect_Reference_table(C 層用于存儲 JNI 局部 / 全局變量)申請內存,申請內存的方法是節點③所示的函數ashmem_create_region(創建一塊 ashmen 匿名共享內存, 并返回一個文件描述符)。節點②代碼節選如下:
(圖 3-11 MemMap:MapAnonymous 節選)
我們線上的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-12 __create_thread_mapped_space 節選)
打印的錯誤信息與圖 [3-7] 中進程邏輯地址占滿導致的 OOM 錯誤信息吻合,圖 [3-7] 中錯誤信息" Try again"說明系統全局錯誤標識 errno 為 11(見圖 [3-10] 系統錯誤定義_errdefs.h).?pthread_create 過程中,節點4相關代碼如下:
(圖 3-13 pthread_create 節選)
此處輸出的錯誤日志"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
POC(Proof of concept) 代碼參見:
https://github.com/piece-the-world/OOMDemo
六,不可思議的OOM,Android高級進階腦圖,全套學習視頻
不可思議的OOM;
高級進階腦圖;
?
加群免費領取安卓進階學習視頻,源碼,面試資料,群內有大牛一起交流討論技術;【964557053】。?
(包括跨平臺開發(Flutter,Weex)、java基礎與原理,自定義控件、NDK、架構設計、性能優化、完整商業項目開發等)
阿里P7高級視頻教程;
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。