溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Android進階性能調(diào)優(yōu);不可思議的OOM

發(fā)布時間:2020-08-03 15:42:30 來源:網(wǎng)絡(luò) 閱讀:880 作者:Android飛魚 欄目:移動開發(fā)

前言;

本文發(fā)現(xiàn)了一類OOM(OutOfMemoryError),這類OOM的特點是崩潰時java堆內(nèi)存和設(shè)備物理內(nèi)存都充足,下文將帶你探索并解釋這類OOM拋出的原因。

文末有demo地址。

關(guān)鍵詞:

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

一、引子

對于每一個移動開發(fā)者,內(nèi)存是都需要小心使用的資源,而線上出現(xiàn)的 OOM(OutOfMemoryError)都會讓開發(fā)者抓狂,因為我們通常仰仗的直觀的堆棧信息對于定位這種問題通常幫助不大。網(wǎng)上有很多資料教我們?nèi)绾巍熬o衣縮食“的利用寶貴的堆內(nèi)存(比如,使用小圖片,bitmap 復(fù)用等),可是:

1.線上的 OOM 真的全是由于堆內(nèi)存緊張導(dǎo)致的嗎?

2.有沒有 App 堆內(nèi)存寬裕,設(shè)備物理內(nèi)存也寬裕的情況下發(fā)生 OOM 的可能?

內(nèi)存充裕的時候出現(xiàn) OOM 崩潰?

3.看似不可思議,然而,最近筆者在調(diào)查一個問題的時候,通過自研的 APM 平臺發(fā)現(xiàn)公司的一個產(chǎn)品的大部分 OOM 確實有這樣的特征,即:OOM 崩潰時,java 堆內(nèi)存遠遠低于 Android 虛擬機設(shè)定的上限,并且物理內(nèi)存充足,SD 卡空間充足

既然內(nèi)存充足,這時候為什么會有 OOM 崩潰呢?

二、問題描述

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

什么導(dǎo)致了 OOM 的產(chǎn)生?

下面是幾個關(guān)于 Android 官方聲明內(nèi)存限制閾值的 API:

Android進階性能調(diào)優(yōu);不可思議的OOM

圖 2-21

通常認為 OOM 發(fā)生是由于 java 堆內(nèi)存不夠用了,即

Android進階性能調(diào)優(yōu);不可思議的OOM

圖 2-2 Java 堆 OOM 產(chǎn)生原因

這種 OOM 可以非常方便的驗證(比如: 通過 new byte[] 的方式嘗試申請超過閾值maxMemory() 的堆內(nèi)存),通常這種 OOM 的錯誤信息通常如下:

Android進階性能調(diào)優(yōu);不可思議的OOM

圖 2-3 堆內(nèi)存不夠?qū)е碌?OOM 的錯誤信息

而前面已經(jīng)提到了,本文中發(fā)現(xiàn)的 OOM 案例中堆內(nèi)存充裕(Runtime.getRuntime().maxMemory() 大小的堆內(nèi)存還剩余很大一部分),設(shè)備當前內(nèi)存也很充裕(ActivityManager.MemoryInfo.availMem 還有很多)。這些 OOM 的錯誤信息大致有下面兩種:

1 . 這種 OOM 在 Android6.0,Android7.0 上各個機型均有發(fā)生,文中簡稱為 OOM?,錯誤信息如下:

Android進階性能調(diào)優(yōu);不可思議的OOM

圖 2-4 OOM 一的錯誤信息

2 . 集中發(fā)生在 Android7.0 及以上的華為手機(EmotionUI_5.0 及以上)的 OOM,簡稱為?OOM 二,對應(yīng)錯誤信息如下:

Android進階性能調(diào)優(yōu);不可思議的OOM

(圖 2-5 OOM 二的錯誤信息)


三、問題分析及解決

3.1代碼分析

Android 系統(tǒng)中,OutOfMemoryError 這個錯誤是怎么被系統(tǒng)拋出的?下面基于 Android6.0 的代碼進行簡單分析:

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

Android進階性能調(diào)優(yōu);不可思議的OOM

圖 3-1 ART Runtime 拋出的位置)

2. 搜索代碼可以發(fā)現(xiàn)以下幾個地方調(diào)用了上述方法拋出 OutOfMemoryError 錯誤

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

Android進階性能調(diào)優(yōu);不可思議的OOM

圖 3-2 Java 堆 OOM

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

1 . 第二個地方是創(chuàng)建線程時

Android進階性能調(diào)優(yōu);不可思議的OOM

圖 3-3 線程創(chuàng)建時 OOM

對比錯誤信息,可以知道我們遇到的 OOM 崩潰就是這個時機,即創(chuàng)建線程的時候(Thread::CreateNativeThread)產(chǎn)生的。

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

那么,我們關(guān)心的就是Thread::CreateNativeThread 時拋出的 OOM 錯誤,創(chuàng)建線程為什么會導(dǎo)致 OOM 呢?

3.2推斷

既然拋出來 OOM,一定是線程創(chuàng)建過程中觸發(fā)了某些我們不知道的限制,既然不是 Art 虛擬機為我們設(shè)置的堆上限,那么可能是更底層的限制。Android 系統(tǒng)基于 linux,所以 linux 的限制對于 Android 同樣適用,這些限制有:

1 ./proc/pid/limits 描述著 linux 系統(tǒng)對對應(yīng)進程的限制,下面是一個樣例:

Android進階性能調(diào)優(yōu);不可思議的OOM

(圖 3-4 Linux 進程限制示例)

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

  • Max stack size,Max processes 的限制是整個系統(tǒng)的,不是針對某個進程的,排除;

  • Max locked memory ,排除,后面會分析,線程創(chuàng)建過程中分配線程私有 stack 使用的 mmap 調(diào)用沒有設(shè)置 MAP_LOCKED,所以這個限制與線程創(chuàng)建過程無關(guān) ;

  • Max pending signals,c 層信號個數(shù)閾值,無關(guān),排除 ;

  • Max msgqueue size,Android IPC 機制不支持消息隊列,排除。

剩下的 limits 項中,Max open files?這一項限制最可疑Max open files 表示?每個進程最大打開文件的數(shù)目,進程?每打開一個文件就會產(chǎn)生一個文件描述符 fd(記錄在 /proc/pid/fd 下面),這個限制表明 fd 的數(shù)目不能超過 Max open files 規(guī)定的數(shù)目。

后面分析線程創(chuàng)建過程中會發(fā)現(xiàn)過程中涉有及到文件描述符。

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

這些限制中與線程相關(guān)的是 /proc/sys/kernel/threads-max,規(guī)定了每個進程創(chuàng)建線程數(shù)目的上限,所以線程創(chuàng)建導(dǎo)致 OOM 的原因也有可能與這個限制相關(guān)。

3.3驗證

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

  • 本地驗證:在本地驗證推斷,試圖復(fù)現(xiàn)與圖 [2-4]OOM 一與圖 [2-5]OOM 二所示錯誤消息一致的 OOM

  • 線上驗收:下發(fā)插件,驗收線上用戶 OOM 時確實是由于上面的推斷的原因?qū)е碌?/span>。

本地驗證

實驗一:?觸發(fā)大量網(wǎng)絡(luò)連接(每個連接處于獨立的線程中)并保持,每打開一個 socket 都會增加一個 fd(/proc/pid/fd 下多一項)

注:不只有這一種增加 fd 數(shù)的方式,也可以用其他方法,比如打開文件,創(chuàng)建 handlerthread 等等

  • 實驗預(yù)期:當進程 fd 數(shù)(可以通過 ls /proc/pid/fd | wc -l 獲得)突破 /proc/pid/limits 中規(guī)定的 Max open files 時,產(chǎn)生 OOM;

  • 實驗結(jié)果:當 fd 數(shù)目到達 /proc/pid/limits 中規(guī)定的 Max open files 時,繼續(xù)開線程確實會導(dǎo)致 OOM 的產(chǎn)生。

錯誤信息及堆棧如下:

Android進階性能調(diào)優(yōu);不可思議的OOM

(圖 3-5 FD 數(shù)超限導(dǎo)致 OOM 的詳細信息)

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

實驗二:創(chuàng)建大量的空線程(不做任何事情,直接 sleep)

  • 實驗預(yù)期:

  • 當線程數(shù)(可以在/proc/pid/status 中的threads項實時查看)超過/proc/sys/kernel/threads-max 中規(guī)定的上限時產(chǎn)生 OOM 崩潰。

  • 實驗結(jié)果:

  • 在 Android7.0 及以上的華為手機(EmotionUI_5.0 及以上)的手機產(chǎn)生 OOM,這些手機的線程數(shù)限制都很小 (應(yīng)該是華為 rom 特意修改的 limits),每個進程只允許最大同時開 500 個線程,因此很容易復(fù)現(xiàn)了。

OOM 時錯誤信息如下:

Android進階性能調(diào)優(yōu);不可思議的OOM

(圖 3-6 線程數(shù)超限導(dǎo)致的 OOM 詳細信息)

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

1 . 其他 Rom 的手機線程數(shù)的上限都比較大,不容易復(fù)現(xiàn)上述問題。但是,對于 32 位的系統(tǒng),當進程的邏輯地址空間不夠的時候也會產(chǎn)生 OOM,每個線程通常需要 mapp 1MB 左右的 stack 空間(stack 大小可以自行設(shè)置),32 為系統(tǒng)進程邏輯地址 4GB,用戶空間少于 3GB。邏輯地址空間不夠(已用邏輯空間地址可以查看 /proc/pid/status 中的 VmPeak/VmSize 記錄),此時創(chuàng)建線程產(chǎn)生的 OOM 具有如下信息:

Android進階性能調(diào)優(yōu);不可思議的OOM

(圖 3-7 邏輯地址空間占滿導(dǎo)致的 OOM)

線上驗收及問題解決

本地嘗試復(fù)現(xiàn)的 OOM 錯誤信息中圖 [3-5] 與線上 OOM 一情況比較吻合,圖 [3-6] 與線上 OOM 二的情況比較吻合,但線上的 OOM 一真的時 FD 數(shù)目超限,OOM 二真的是由于華為手機線程數(shù)超限的原因?qū)е碌膯??最終確定還需要取線上設(shè)備的數(shù)據(jù)進行驗證。

驗證方法:

下發(fā)插件到線上用戶,當 Thread.UncaughtExceptionHandler 捕獲到OutOfMemoryError 時記錄 /proc/pid 目錄下的如下信息:

1. /proc/pid/fd 目錄下文件數(shù) (fd 數(shù))

2. /proc/pid/status 中 threads 項(當前線程數(shù)目)

3.?OOM 的日志信息(出了堆棧信息還包含其他的一些 warning 信息

線上 OOM 一驗證

發(fā)生 OOM 一的線上設(shè)備中采集到的信息:

1. /proc/pid/fd 目錄下文件數(shù)與 /proc/pid/limits 中的 Max open files 數(shù)目持平,證明 FD 數(shù)目已經(jīng)滿了;

2. 崩潰時日志信息與圖 [3-5] 基本一致;

由此,證明?線上的 OOM 一確實是由于 FD 數(shù)目過多導(dǎo)致的 OOM,推斷驗證成功。

OOM 一的定位與解決:

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

線上 OOM 二驗證?集中在華為系統(tǒng)的 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] 基本一致;

推斷驗證成功,即?線程數(shù)受限導(dǎo)致創(chuàng)建線程時 clone failed 導(dǎo)致了線上的 OOM 二。

OOM 二的定位與解決:

關(guān)于 App 業(yè)務(wù)代碼中的問題還在定位修復(fù)中。

3.4解釋

下面從代碼分析本文描述的 OOM 是怎么發(fā)生的,首先線程創(chuàng)建的簡易版流程圖如下所示:

Android進階性能調(diào)優(yōu);不可思議的OOM

(圖 3-8 線程創(chuàng)建流程)

上圖中,線程創(chuàng)建大概有兩個關(guān)鍵的步驟:

  • 第一列中的?創(chuàng)建線程私有的結(jié)構(gòu)體 JNIENV(JNI 執(zhí)行環(huán)境,用于 C 層調(diào)用 Java 層代碼)

  • 第二列中的?調(diào)用 posix C 庫的函數(shù) pthread_create 進行線程創(chuàng)建工作

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

1. 圖中節(jié)點①,/art/runtime/thread.cc 中的函數(shù)Thread:CreateNativeThread部分節(jié)選代碼如下:

Android進階性能調(diào)優(yōu);不可思議的OOM

(圖 3-9 Thread:CreateNativeThread 節(jié)選)

可知:

  • JNIENV 創(chuàng)建不成功時產(chǎn)生 OOM 的錯誤信息為 "Could not allocate JNI Env",與文中 OOM 一一致

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

Android進階性能調(diào)優(yōu);不可思議的OOM

圖 3-10 系統(tǒng)錯誤定義 _errdefs.h

2. 圖中節(jié)點②和③是創(chuàng)建 JNIENV 過程的關(guān)鍵節(jié)點,節(jié)點②/art/runtime/mem_map.cc 中 函數(shù) MemMap:MapAnonymous 的作用是為 JNIENV 結(jié)構(gòu)體中Indirect_Reference_table(C 層用于存儲 JNI 局部 / 全局變量)申請內(nèi)存,申請內(nèi)存的方法是節(jié)點③所示的函數(shù)ashmem_create_region(創(chuàng)建一塊 ashmen 匿名共享內(nèi)存, 并返回一個文件描述符)。節(jié)點②代碼節(jié)選如下:

Android進階性能調(diào)優(yōu);不可思議的OOM

(圖 3-11 MemMap:MapAnonymous 節(jié)選)

我們線上的OOM 一的錯誤信息"ashmem_create_region failed for 'indirect ref table': Too many open files",與此處打印的信息吻合。"Too many open files"的錯誤描述說明此處的 errno(系統(tǒng)全局錯誤標識)為 24(見圖 [3-10] 系統(tǒng)錯誤定義 _errdefs.h)。由此看出我們線上的 OOM 一是由于文件描述符數(shù)目已滿,ashmem_create_region?無法返回新的 FD 而導(dǎo)致的。

3. 圖中節(jié)點④和⑤是調(diào)用 C 庫創(chuàng)建線程時的環(huán)節(jié),創(chuàng)建線程首先 調(diào)用 __allocate_thread 函數(shù)申請線程私有的棧內(nèi)存 (stack) 等,然后 調(diào)用 clone 方法進行線程創(chuàng)建.申請 stack 采用的時 mmap 的方式,節(jié)點⑤代碼節(jié)選如下:

Android進階性能調(diào)優(yōu);不可思議的OOM

(圖 3-12 __create_thread_mapped_space 節(jié)選)

打印的錯誤信息與圖 [3-7] 中進程邏輯地址占滿導(dǎo)致的 OOM 錯誤信息吻合,圖 [3-7] 中錯誤信息" Try again"說明系統(tǒng)全局錯誤標識 errno 為 11(見圖 [3-10] 系統(tǒng)錯誤定義_errdefs.h).?pthread_create 過程中,節(jié)點4相關(guān)代碼如下:

Android進階性能調(diào)優(yōu);不可思議的OOM

(圖 3-13 pthread_create 節(jié)選)

此處輸出的錯誤日志"pthread_create failed: clone failed: %s"與我們線上發(fā)現(xiàn)的 OOM 二吻合,圖 [3-6] 中的錯誤描述" Out of memory"說明系統(tǒng)全局錯誤標識 errno 為 12(見圖 [3-10] 系統(tǒng)錯誤定義 _errdefs.h)。?由此線上的?OOM 二就是由于線程數(shù)的限制而在節(jié)點 5 clone 失敗導(dǎo)致 OOM。


四、結(jié)論及監(jiān)控

4.1導(dǎo)致OOM發(fā)生的原因

綜上,可以導(dǎo)致 OOM 的原因有以下幾種:

1.?文件描述符 (fd) 數(shù)目超限,即 proc/pid/fd 下文件數(shù)目突破 /proc/pid/limits 中的限制??赡艿陌l(fā)生場景有:短時間內(nèi)大量請求導(dǎo)致 socket 的 fd 數(shù)激增,大量(重復(fù))打開文件等 ;

2.?線程數(shù)超限,即proc/pid/status中記錄的線程數(shù)(threads 項)突破 /proc/sys/kernel/threads-max 中規(guī)定的最大線程數(shù)??赡艿陌l(fā)生場景有:app 內(nèi)多線程使用不合理,如多個不共享線程池的 OKhttpclient 等等 ;

3. 傳統(tǒng)的 java 堆內(nèi)存超限,即申請堆內(nèi)存大小超過了Runtime.getRuntime().maxMemory();

4. (低概率)32 為系統(tǒng)進程邏輯空間被占滿導(dǎo)致 OOM;

5. 其他。

4.2監(jiān)控措施

可以利用 linux 的 inotify 機制進行監(jiān)控:

  • watch /proc/pid/fd來監(jiān)控 app 打開文件的情況,

  • watch /proc/pid/task來監(jiān)控線程使用情況。


五、Demo

POC(Proof of concept) 代碼參見:

https://github.com/piece-the-world/OOMDemo

六,不可思議的OOM,Android高級進階腦圖,全套學習視頻

不可思議的OOM;

Android進階性能調(diào)優(yōu);不可思議的OOM


高級進階腦圖;

Android進階性能調(diào)優(yōu);不可思議的OOM

?


加群免費領(lǐng)取安卓進階學習視頻,源碼,面試資料,群內(nèi)有大牛一起交流討論技術(shù);【964557053】。?

(包括跨平臺開發(fā)(Flutter,Weex)、java基礎(chǔ)與原理,自定義控件、NDK、架構(gòu)設(shè)計、性能優(yōu)化、完整商業(yè)項目開發(fā)等)


阿里P7高級視頻教程;

Android進階性能調(diào)優(yōu);不可思議的OOM

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI