什么是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虛擬機(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)單分析:
- 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";
-
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)生異常的原因。
- 線程數(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)存占用
- 使用更加輕量的數(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)裝箱灌闺,并且避免了裝箱后的解箱艰争。 - 避免在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里面使用枚舉桂对。 - 減小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睦柴,存在很大差異。 - 使用更小的圖片
對(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ù)使用
- 注意在ListView/GridView等出現(xiàn)大量重復(fù)子組件的視圖里面對(duì)ConvertView的復(fù)用
- Bitmap對(duì)象的復(fù)用
在RecyclerView、ListView尖奔、GridView等顯示大量圖片的控件里面需要使用LRU機(jī)制來(lái)緩存處理好的Bitmap搭儒。
利用inBitmap的高級(jí)特性提高Android系統(tǒng)在Bitmap分配與釋放執(zhí)行效率上的提升。 - 避免在onDraw方法里面執(zhí)行對(duì)象的創(chuàng)建
類似onDraw等頻繁調(diào)用的方法提茁,一定需要注意避免在這里做創(chuàng)建對(duì)象的操作淹禾,因?yàn)樗鼤?huì)迅速增加內(nèi)存的使用,而且很容易引起頻繁的GC茴扁,甚至是內(nèi)存抖動(dòng)铃岔。 - StringBuilder
當(dāng)代碼中需要使用到大量的字符串拼接操作,就有必要考慮使用StringBuilder來(lái)代替頻繁的”+“峭火。
避免對(duì)象的內(nèi)存泄漏
- 注意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ā)生泄漏坯苹。 - 考慮使用Application Context而不是Activity Context
對(duì)于大部分非必須使用Activity Context的情況(Dialog的Context就必須是Activity Context)隆檀,都可以考慮使用Application Context而不是Activity的Context,這樣就可以避免不經(jīng)意的Activity泄漏粹湃。 - 注意臨時(shí)Bitmap對(duì)象的及時(shí)回收
臨時(shí)創(chuàng)建的某個(gè)相對(duì)比較大的bitmap對(duì)象恐仑,在經(jīng)過(guò)轉(zhuǎn)換得到新的bitmap對(duì)象之后,應(yīng)該盡快回收原始的bitmap为鳄,這樣能夠更快釋放原始bitmap所占用的空間裳仆。 - 注意監(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纯丸。 - 注意緩存容器中的對(duì)象泄漏
如果容器是靜態(tài)或者全局的,那么對(duì)于里面存放的對(duì)象要及時(shí)remove静袖。 - 注意WebView的泄漏
Android中WebView存在很大的兼容性問(wèn)題觉鼻,需要再合適的時(shí)機(jī)進(jìn)行銷毀。 - 注意Cursor對(duì)象是否及時(shí)關(guān)閉
對(duì)于數(shù)據(jù)庫(kù)查詢的Cursor队橙,如果沒(méi)有及時(shí)關(guān)閉就會(huì)造成泄漏坠陈。
內(nèi)存使用策略優(yōu)化
- Try catch 某些大內(nèi)存的操作
在某些情況下,我們需要事先評(píng)估那些可能發(fā)生OOM的代碼捐康,對(duì)于這些可能發(fā)生OOM的代碼仇矾,加入catch機(jī)制,可以考慮在catch里面嘗試一次降級(jí)的內(nèi)存分配操作解总。例如decode bitmap的時(shí)候贮匕,catch到OOM,可以嘗試把采樣比例再增加一倍之后倾鲫,再次嘗試decode粗合。 - 謹(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ì)待了漓雅。 - 特別留意單例模式的不合理持有
- 優(yōu)化布局層次,減少內(nèi)存消耗
越扁平化的視圖布局朽色,占用的內(nèi)存就越少邻吞,效率越高。我們需要盡量保證布局足夠扁平化葫男,當(dāng)使用系統(tǒng)提供的View無(wú)法實(shí)現(xiàn)足夠扁平的時(shí)候考慮使用自定義View來(lái)達(dá)到目的抱冷。