Android OOM異常分析

OOM目錄.png

什么是OOM瓤湘?

OOM全稱為OutOfMemoryError,解釋為內(nèi)存溢出恩尾,是Android開(kāi)發(fā)中常見(jiàn)的一種錯(cuò)誤弛说,這種錯(cuò)誤在線上Crash中占比很大一部分,不像NullPointException似的容易定位問(wèn)題翰意,OOM解決起來(lái)相對(duì)于一般的Exception或者Error都要難一些木人,主要是由于錯(cuò)誤產(chǎn)生的root cause不是很顯而易見(jiàn)。
在分析OOM之前冀偶,先回顧一下Java的內(nèi)存區(qū)域醒第,根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定,運(yùn)行時(shí)數(shù)據(jù)區(qū)通常包括這幾個(gè)部分:程序計(jì)數(shù)器(Program Counter Register)进鸠、Java棧(VM Stack)稠曼、本地方法棧(Native Method Stack)、方法區(qū)(Method Area)堤如、堆(Heap)蒲列。


java內(nèi)存區(qū)域.jpg

在Java虛擬機(jī)規(guī)范的描述中,除了程序計(jì)數(shù)器外搀罢,虛擬機(jī)內(nèi)存的其他幾個(gè)運(yùn)行時(shí)區(qū)域都有發(fā)生OOM的異郴柔可能。

導(dǎo)致OOM的原因

Android系統(tǒng)中榔至,OutOfMemoryError這個(gè)錯(cuò)誤是怎么被系統(tǒng)拋出的抵赢?下面基于Android6.0的代碼進(jìn)行簡(jiǎn)單分析:


Android源碼拋異常處.png
  1. heap.cc是在Android中需要分配的內(nèi)存大于可用的內(nèi)存會(huì)導(dǎo)致OOM姑蓝,其最大內(nèi)存ActivityManager.getMemoryClass()獲得,這也是Android中最常見(jiàn)的OOM類型媳叨,會(huì)拋出異常信息万牺。
void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type)
拋出時(shí)的錯(cuò)誤信息:
    oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free  << " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM";
  1. thread.cc文件是創(chuàng)建線程時(shí)拋出的OOM錯(cuò)誤,且有多種錯(cuò)誤信息邢享,主要的流程如圖所示鹏往,有四個(gè)點(diǎn)會(huì)產(chǎn)生異常信息。


    線程創(chuàng)建過(guò)程.png

創(chuàng)建JNI失敗

創(chuàng)建JNIEnv可以歸為兩個(gè)步驟:
第一步創(chuàng)建匿名共享內(nèi)存時(shí)骇塘,需要打開(kāi)/dev/ashmem文件伊履,所以需要一個(gè)FD(文件描述符)。此時(shí)款违,如果創(chuàng)建的FD數(shù)已經(jīng)達(dá)到上限唐瀑,則會(huì)導(dǎo)致創(chuàng)建JNIEnv失敗,拋出錯(cuò)誤信息如下:

E/art: ashmem_create_region failed for 'indirect ref table': Too many open files
 java.lang.OutOfMemoryError: Could not allocate JNI Env
   at java.lang.Thread.nativeCreate(Native Method)
   at java.lang.Thread.start(Thread.java:730)

第二步調(diào)用mmap時(shí)插爹,如果進(jìn)程虛擬內(nèi)存地址空間耗盡哄辣,也會(huì)導(dǎo)致創(chuàng)建JNIEnv失敗,拋出錯(cuò)誤信息如下:

E/art: Failed anonymous mmap(0x0, 8192, 0x3, 0x2, 116, 0): Operation not permitted. See process maps in the log.
java.lang.OutOfMemoryError: Could not allocate JNI Env
  at java.lang.Thread.nativeCreate(Native Method)
  at java.lang.Thread.start(Thread.java:1063)

創(chuàng)建線程失敗

創(chuàng)建線程也可以歸納為兩個(gè)步驟:
第一步分配棧內(nèi)存失敗是由于進(jìn)程的虛擬內(nèi)存不足赠尾,拋出錯(cuò)誤信息如下:

W/libc: pthread_create failed: couldn't allocate 1073152-bytes mapped space: Out of memory
W/tch.crowdsourc: Throwing OutOfMemoryError with VmSize  4191668 kB "pthread_create (1040KB stack) failed: Try again"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again
        at java.lang.Thread.nativeCreate(Native Method)
        at java.lang.Thread.start(Thread.java:753)

第二步clone方法失敗是因?yàn)榫€程數(shù)超出了限制力穗,拋出錯(cuò)誤信息如下:

W/libc: pthread_create failed: clone failed: Out of memory
W/art: Throwing OutOfMemoryError "pthread_create (1040KB stack) failed: Out of memory"
java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Out of memory
  at java.lang.Thread.nativeCreate(Native Method)
  at java.lang.Thread.start(Thread.java:1078)

如何排查OOM

常用的OOM檢測(cè)工具介紹:

工具 問(wèn)題 能力
top 內(nèi)存占用 發(fā)現(xiàn)
meminfo Native內(nèi)存泄漏,Activity泄漏萍虽,數(shù)據(jù)庫(kù)緩存命中率 發(fā)現(xiàn)+初步定位
MAT 內(nèi)存泄漏 發(fā)現(xiàn)+定位
LeakCanary Activity內(nèi)存泄漏 自動(dòng)發(fā)現(xiàn)+ 定位
StrictMode Activity內(nèi)存泄漏 自動(dòng)發(fā)現(xiàn)+ 初步定位
DDMS 申請(qǐng)內(nèi)存次數(shù)過(guò)多睛廊、輔助定位GC 發(fā)現(xiàn)+ 定位

在分析清楚OOM產(chǎn)生的原因之后,可以根據(jù)堆棧信息的特征來(lái)確定這是哪一個(gè)類型的OOM杉编,根據(jù)以上的異吵可以定位產(chǎn)生異常的原因。

  1. 線程數(shù)量超出限制
    對(duì)于這類異常邓馒,是proc/pid/status中記錄的線程數(shù)(threads項(xiàng))突破/proc/sys/kernel/threads-max中規(guī)定的最大線程數(shù)嘶朱,或者虛擬內(nèi)存耗盡導(dǎo)致的,通過(guò)Thread.getAllStackTraces()可以得到進(jìn)程中的所有線程以及對(duì)應(yīng)的堆棧信息光酣,就可以定位到到這類線程的創(chuàng)建時(shí)機(jī)疏遏,就能知道問(wèn)題所在。如果線程是有自定義名稱的救军,那么直接就可以在代碼中搜索到創(chuàng)建線程的位置财异,從而定位問(wèn)題,如果線程創(chuàng)建時(shí)沒(méi)有指定名稱唱遭,那么就需要通過(guò)該線程的堆棧信息來(lái)輔助定位戳寸。

2.FD數(shù)量超出限制
在/proc/pid/limits描述著Linux系統(tǒng)對(duì)對(duì)應(yīng)進(jìn)程的限制,其中Max open files就代表可創(chuàng)建FD的最大數(shù)目拷泽。進(jìn)程中創(chuàng)建的FD記錄在/proc/pid/fd中疫鹊,可以得到FD的信息袖瞻。然后可以查出FD指向的文件,有可能是Socket拆吆,F(xiàn)ile聋迎,就可以在代碼中定位到出問(wèn)題的代碼。

3.Java堆溢出
堆內(nèi)存分配失敗枣耀,是Android最常見(jiàn)的OOM霉晕,通常說(shuō)明進(jìn)程中大部分的內(nèi)存已經(jīng)被占用了,且不能被垃圾回收器回收奕枢,一般來(lái)說(shuō)此時(shí)內(nèi)存占用都存在一些問(wèn)題娄昆,例如內(nèi)存泄漏等。要想定位到問(wèn)題所在缝彬,就需要知道進(jìn)程中的內(nèi)存都被哪些對(duì)象占用,以及這些對(duì)象的引用鏈路哺眯。而這些信息都可以在Java內(nèi)存快照文件中得到谷浅,調(diào)用Debug.dumpHprofData(String fileName)函數(shù)就可以得到當(dāng)前進(jìn)程的Java內(nèi)存快照文件(即HPROF文件),然后可以通過(guò)MAT工具進(jìn)行分析奶卓,根據(jù)GC root的調(diào)用鏈一疯,找到占用內(nèi)存的對(duì)象,然后定位到代碼的位置夺姑。

Android開(kāi)發(fā)中有一個(gè)庫(kù)LeakCanary墩邀,可以用來(lái)自動(dòng)分析Activity泄漏,主要的原理是

  • 當(dāng)一個(gè)Activity Destory之后盏浙,將它放在一個(gè)WeakReference弱引用中中
  • 把這個(gè)WeakReference關(guān)聯(lián)到一個(gè)ReferenceQueue
  • 查看ReferenceQueue中是否存在Activity的引用
  • 如果Activity泄露了眉睹,就Dump出heap信息,然后去分析內(nèi)存泄露的路徑

如何避免OOM

在實(shí)踐中有什么方法來(lái)減少OOM的出現(xiàn)呢废膘?總結(jié)下來(lái)大概分下面幾個(gè)方面:

  • 減小對(duì)象的內(nèi)存占用
  • 內(nèi)存對(duì)象的重復(fù)使用
  • 避免對(duì)象的內(nèi)存泄漏
  • 內(nèi)存使用策略優(yōu)化

減小對(duì)象的內(nèi)存占用

  1. 使用更加輕量的數(shù)據(jù)結(jié)構(gòu)
    使用ArrayMap/SparseArray替代HashMap等傳統(tǒng)數(shù)據(jù)結(jié)構(gòu)竹海。
    ArrayMap是Android系統(tǒng)專為移動(dòng)操作系統(tǒng)編寫(xiě)的容器,在大多數(shù)情況下丐黄,比HashMap效率更高斋配,占用內(nèi)存更少。
    SparseArray更加高效在于它們避免了對(duì)key和value的autobox自動(dòng)裝箱灌闺,并且避免了裝箱后的解箱艰争。
  2. 避免在Android里面使用Enum
    Android官方說(shuō)明”Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.“,所以應(yīng)避免在Android里面使用枚舉桂对。
  3. 減小Bitmap對(duì)象的內(nèi)存占用
    Bitmap是一個(gè)極容易消耗內(nèi)存的大胖子甩卓,減小創(chuàng)建處理的Bitmap的內(nèi)存占用是很重要的,通常來(lái)說(shuō)有下面2個(gè)措施:
    inSampleSize:縮放比例接校,在把圖片載入內(nèi)存之前猛频,我們需要先計(jì)算出一個(gè)合適的縮放比例狮崩,避免不必要的大圖載入。
    decode format:解碼格式鹿寻,選擇ARGB_8888/RGB_565/ARGB_4444/ALPHA_8睦柴,存在很大差異。
  4. 使用更小的圖片
    對(duì)應(yīng)資源圖片毡熏,要特別留意這張圖片是否存在可壓縮的空間坦敌,是否可以使用一張更小的圖片。盡量使用更小的圖片不僅僅可以減少內(nèi)存的使用痢法,還可以避免出現(xiàn)大量的InflationException狱窘。假設(shè)有一張很大的圖片被XML文件直接引用,很有可能在初始化視圖的時(shí)候會(huì)因?yàn)閮?nèi)存不足而發(fā)生InflationException财搁,這個(gè)問(wèn)題的根本原因其實(shí)是發(fā)生了OOM蘸炸。

內(nèi)存對(duì)象的重復(fù)使用

  1. 注意在ListView/GridView等出現(xiàn)大量重復(fù)子組件的視圖里面對(duì)ConvertView的復(fù)用
  2. Bitmap對(duì)象的復(fù)用
    在RecyclerView、ListView尖奔、GridView等顯示大量圖片的控件里面需要使用LRU機(jī)制來(lái)緩存處理好的Bitmap搭儒。
    利用inBitmap的高級(jí)特性提高Android系統(tǒng)在Bitmap分配與釋放執(zhí)行效率上的提升。
  3. 避免在onDraw方法里面執(zhí)行對(duì)象的創(chuàng)建
    類似onDraw等頻繁調(diào)用的方法提茁,一定需要注意避免在這里做創(chuàng)建對(duì)象的操作淹禾,因?yàn)樗鼤?huì)迅速增加內(nèi)存的使用,而且很容易引起頻繁的GC茴扁,甚至是內(nèi)存抖動(dòng)铃岔。
  4. StringBuilder
    當(dāng)代碼中需要使用到大量的字符串拼接操作,就有必要考慮使用StringBuilder來(lái)代替頻繁的”+“峭火。

避免對(duì)象的內(nèi)存泄漏

  1. 注意Activity的泄漏
    通常來(lái)說(shuō)毁习,Activity的泄漏是內(nèi)存泄漏里面最為嚴(yán)重的問(wèn)題,它占用的內(nèi)存最多躲胳,影響面廣蜓洪。
    導(dǎo)致Activity泄漏的兩種情況:
    內(nèi)部類引用導(dǎo)致Activity的泄漏
    Activity Context被傳遞到其他實(shí)例中,這可能導(dǎo)致自身被引用而發(fā)生泄漏坯苹。
  2. 考慮使用Application Context而不是Activity Context
    對(duì)于大部分非必須使用Activity Context的情況(Dialog的Context就必須是Activity Context)隆檀,都可以考慮使用Application Context而不是Activity的Context,這樣就可以避免不經(jīng)意的Activity泄漏粹湃。
  3. 注意臨時(shí)Bitmap對(duì)象的及時(shí)回收
    臨時(shí)創(chuàng)建的某個(gè)相對(duì)比較大的bitmap對(duì)象恐仑,在經(jīng)過(guò)轉(zhuǎn)換得到新的bitmap對(duì)象之后,應(yīng)該盡快回收原始的bitmap为鳄,這樣能夠更快釋放原始bitmap所占用的空間裳仆。
  4. 注意監(jiān)聽(tīng)器的注銷
    在Android程序里面存在很多需要register和unregister的監(jiān)聽(tīng)器,需要確保在合適的時(shí)候及時(shí)unregister那些監(jiān)聽(tīng)器孤钦。手動(dòng)add的listener歧斟,需要記得及時(shí)remove這個(gè)listener纯丸。
  5. 注意緩存容器中的對(duì)象泄漏
    如果容器是靜態(tài)或者全局的,那么對(duì)于里面存放的對(duì)象要及時(shí)remove静袖。
  6. 注意WebView的泄漏
    Android中WebView存在很大的兼容性問(wèn)題觉鼻,需要再合適的時(shí)機(jī)進(jìn)行銷毀。
  7. 注意Cursor對(duì)象是否及時(shí)關(guān)閉
    對(duì)于數(shù)據(jù)庫(kù)查詢的Cursor队橙,如果沒(méi)有及時(shí)關(guān)閉就會(huì)造成泄漏坠陈。

內(nèi)存使用策略優(yōu)化

  1. Try catch 某些大內(nèi)存的操作
    在某些情況下,我們需要事先評(píng)估那些可能發(fā)生OOM的代碼捐康,對(duì)于這些可能發(fā)生OOM的代碼仇矾,加入catch機(jī)制,可以考慮在catch里面嘗試一次降級(jí)的內(nèi)存分配操作解总。例如decode bitmap的時(shí)候贮匕,catch到OOM,可以嘗試把采樣比例再增加一倍之后倾鲫,再次嘗試decode粗合。
  2. 謹(jǐn)慎使用static 對(duì)象
    static是Java中的一個(gè)關(guān)鍵字,當(dāng)用它來(lái)修飾成員變量時(shí)乌昔,那么該變量就屬于該類,而不是該類的實(shí)例壤追。 不少程序員喜歡用static這個(gè)關(guān)鍵字修飾變量磕道,因?yàn)樗沟米兞康纳芷诖蟠笱娱L(zhǎng)啦,并且訪問(wèn)的時(shí)候行冰,也極其的方便溺蕉,用類名就能直接訪問(wèn),各個(gè)資源間 傳值也極其的方便悼做,所以疯特,它經(jīng)常被我們使用。但如果用它來(lái)引用一些資源耗費(fèi)過(guò)多的實(shí)例(Context的情況最多)肛走,這時(shí)就要謹(jǐn)慎對(duì)待了漓雅。
  3. 特別留意單例模式的不合理持有
  4. 優(yōu)化布局層次,減少內(nèi)存消耗
    越扁平化的視圖布局朽色,占用的內(nèi)存就越少邻吞,效率越高。我們需要盡量保證布局足夠扁平化葫男,當(dāng)使用系統(tǒng)提供的View無(wú)法實(shí)現(xiàn)足夠扁平的時(shí)候考慮使用自定義View來(lái)達(dá)到目的抱冷。

參考鏈接

Android OOM 分析
Probe:Android線上OOM問(wèn)題定位組件

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市梢褐,隨后出現(xiàn)的幾起案子旺遮,更是在濱河造成了極大的恐慌赵讯,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耿眉,死亡現(xiàn)場(chǎng)離奇詭異边翼,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)跷敬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)讯私,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人西傀,你說(shuō)我怎么就攤上這事斤寇。” “怎么了拥褂?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵娘锁,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我饺鹃,道長(zhǎng)莫秆,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任悔详,我火速辦了婚禮镊屎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘茄螃。我一直安慰自己缝驳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布归苍。 她就那樣靜靜地躺著用狱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拼弃。 梳的紋絲不亂的頭發(fā)上夏伊,一...
    開(kāi)封第一講書(shū)人閱讀 49,772評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音吻氧,去河邊找鬼溺忧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛医男,可吹牛的內(nèi)容都是我干的砸狞。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼镀梭,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼刀森!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起报账,我...
    開(kāi)封第一講書(shū)人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤研底,失蹤者是張志新(化名)和其女友劉穎埠偿,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體榜晦,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡冠蒋,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了乾胶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抖剿。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖识窿,靈堂內(nèi)的尸體忽然破棺而出斩郎,到底是詐尸還是另有隱情,我是刑警寧澤喻频,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布缩宜,位于F島的核電站,受9級(jí)特大地震影響甥温,放射性物質(zhì)發(fā)生泄漏锻煌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一姻蚓、第九天 我趴在偏房一處隱蔽的房頂上張望宋梧。 院中可真熱鬧,春花似錦狰挡、人聲如沸乃秀。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至枢贿,卻和暖如春殉农,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背局荚。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工超凳, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耀态。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓轮傍,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親首装。 傳聞我的和親對(duì)象是個(gè)殘疾皇子创夜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348