OOM 的產(chǎn)生
在使用C或C++語(yǔ)言時(shí)峻村,我們可操作的內(nèi)存空間就是整個(gè)設(shè)備的物理內(nèi)存超歌,程序員需要自己聲明內(nèi)存空間专普,也需要自己在恰當(dāng)?shù)臅r(shí)機(jī)釋放掉內(nèi)存,一旦出錯(cuò)就會(huì)造成內(nèi)存泄漏码耐。而Java語(yǔ)言為了解決這個(gè)問(wèn)題追迟,在操作系統(tǒng)之上創(chuàng)造了一個(gè)Java虛擬機(jī)(JVM),讓Java語(yǔ)言編譯后的字節(jié)碼運(yùn)行在此虛擬機(jī)之上伐坏。啟動(dòng)一個(gè)Java應(yīng)用怔匣,會(huì)首先啟動(dòng)JVM握联,JVM 會(huì)向操作系統(tǒng)申請(qǐng)所需內(nèi)存桦沉,然后把內(nèi)存分成為棧內(nèi)存和堆內(nèi)存每瞒。堆內(nèi)存用以存放對(duì)象實(shí)例,并可被Java回收機(jī)制回收纯露,一旦剩余堆內(nèi)存空間不夠申請(qǐng)新對(duì)象時(shí)就會(huì)產(chǎn)生OutOfMemoryError異常剿骨。
Android內(nèi)存管理
Android的Dalvik虛擬機(jī)(DVM)是參考JVM做出來(lái)的,所以大同小異埠褪。最主要的兩個(gè)區(qū)別是:一浓利,DVM 基于寄存器,而JVM基于棧來(lái)進(jìn)行局部變量的操作钞速,當(dāng)然在性能上DVM會(huì)更快贷掖;二,在DVM上運(yùn)行的是被進(jìn)一步處理的JAVA字節(jié)碼渴语,后綴為.dex苹威,.dex 是把Java應(yīng)用中所有的.class文件合并而成,縮減了包的體積驾凶。Android中的 DVM 如 JVM 一樣對(duì)每個(gè)應(yīng)用可使用的最大內(nèi)存空間做了限制牙甫,每臺(tái)設(shè)備出廠之前廠家就對(duì)單個(gè) DVM 實(shí)例可使用的最大內(nèi)存進(jìn)行了限定。Android屆的第一款手機(jī)HTC G1的大小為16M调违。這些信息儲(chǔ)存在手機(jī)中 /system/build.prop
配置文件中窟哺,如下我這是我使用的華為6p plus手機(jī)的相關(guān)信息
adb shell
shell@hwPE:/ $ cat /system/build.prop
...
dalvik.vm.heapstartsize=8m
dalvik.vm.heapgrowthlimit=192m
dalvik.vm.heapsize=512m
dalvik.vm.heaptargetutilization=0.75
dalvik.vm.heapminfree=512k
dalvik.vm.heapmaxfree=8m
...
dalvik.vm.heapstartsize
為一個(gè)應(yīng)用初始分配的堆大小,越大意味著應(yīng)用第一次啟動(dòng)時(shí)越流暢技肩,但也意味著內(nèi)存耗用越快且轨。
dalvik.vm.heapgrowthlimit
這就是所謂的單個(gè)應(yīng)用可使用的最大內(nèi)存堆大小。
dalvik.vm.heapsize
此項(xiàng)表示應(yīng)用在manifest中配置android:largeHeap="true"時(shí)可使用的最大內(nèi)存堆大小亩鬼。
獲取內(nèi)存配置
有些小伙伴可能使用過(guò)以下方法來(lái)獲得內(nèi)存信息殖告,但可能就和最初的我一樣,不知道這些獲得的數(shù)據(jù)到底是啥意思雳锋。下面我們就來(lái)說(shuō)說(shuō)每個(gè)方法所獲得的數(shù)據(jù)的意義和特點(diǎn)黄绩。
Log.e("pengtao", "max memory = " + Runtime.getRuntime().maxMemory());
Log.e("pengtao", "free memory = " + Runtime.getRuntime().freeMemory());
Log.e("pengtao", "total memory = " + Runtime.getRuntime().totalMemory());
ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
Log.e("pengtao", "memoryClass = " + Integer.toString(am.getMemoryClass()));
Log.e("pengtao", "largememoryClass = " + Integer.toString(am.getLargeMemoryClass()));
Runtime.getRuntime().maxMemory()
這個(gè)參數(shù)對(duì)應(yīng)到build.prop中的信息就是在未設(shè)置largeHeap為true時(shí)會(huì)返回heapgrowthlimit
的大小,而設(shè)置了largeHeap為true后玷过,則返回heapsize
大小爽丹。單位為Bytes。
getMemoryClass
所獲得的大小不受largeHeap配置影響辛蚊,永遠(yuǎn)是heapgrowthlimit
中大小粤蝎。而getLargeMemoryClass
則為heapsize
大小,兩者單位都為M袋马。
最后要想理解totalMemory
和freeMemory
概念可以看下下圖初澎。
上述日志代碼執(zhí)行后,在我手機(jī)上跑出來(lái)的結(jié)果如下(設(shè)置了largeHeap為true),可以把這些數(shù)據(jù)與build.prop
中的數(shù)據(jù)對(duì)應(yīng)起來(lái):
12-05 16:01:50.346 29178-29178/? E/pengtao: max memory = 536870912
12-05 16:01:50.346 29178-29178/? E/pengtao: free memory = 8201514
12-05 16:01:50.346 29178-29178/? E/pengtao: total memory = 25361266
12-05 16:01:50.346 29178-29178/? E/pengtao: memoryClass = 192
12-05 16:01:50.346 29178-29178/? E/pengtao: largememoryClass = 512
注:謹(jǐn)慎設(shè)置largeHeap碑宴,因?yàn)樵酱蟮亩芽臻g意味著GC(垃圾回收)需要遍歷的對(duì)象越多软啼,時(shí)間就會(huì)越久。不過(guò)largeHeap配置是從Android 3.0開(kāi)始支持的延柠,而并發(fā)式的GC是從Android 2.3后開(kāi)始支持祸挪,所以雖說(shuō)GC時(shí)間變久了,但不會(huì)對(duì)應(yīng)用運(yùn)行造成很大影響贞间。
Android中OOM
Android應(yīng)用與Java應(yīng)用一樣贿条,避免OOM就是要剩余足夠堆內(nèi)存供應(yīng)用使用,要想內(nèi)存足夠呢增热,首先就需要避免應(yīng)用存在內(nèi)存泄漏的情況整以,內(nèi)存泄漏后,可使用的內(nèi)存空間減少峻仇,自然就會(huì)更容易產(chǎn)生OOM悄蕾。關(guān)于如何避免內(nèi)存泄漏,可以移步到本人寫得另一篇文章《Android中常見(jiàn)的內(nèi)存泄漏》中础浮。還有一個(gè)容易產(chǎn)生OOM的情況帆调,就是加載大數(shù)據(jù)到內(nèi)存中。要想更深入理解這一點(diǎn)豆同,讓我們來(lái)做個(gè)簡(jiǎn)單應(yīng)用番刊,以下為其核心代碼:
final List<byte[]> container = new ArrayList<>();
findViewById(R.id.get_memory).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.e("pengtao", "max memory = " + Runtime.getRuntime().maxMemory());
Log.e("pengtao", "free memory = " + Runtime.getRuntime().freeMemory());
Log.e("pengtao", "total memory = " + Runtime.getRuntime().totalMem
byte[] b = new byte[100 * 1000 * 1000];
container.add(b);
}
});
同樣在這段代碼運(yùn)行在一個(gè)堆限制為192M的手機(jī)上,當(dāng)點(diǎn)擊兩次按鈕影锈,應(yīng)用崩潰芹务,打印的信息如下:
12-06 21:17:44.925 4021-4021/? E/pengtao: max memory = 201326592
12-06 21:17:44.925 4021-4021/? E/pengtao: free memory = 16761904
12-06 21:17:44.925 4021-4021/? E/pengtao: total memory = 132736208
12-06 21:17:44.925 4021-4021/? I/art: Starting a blocking GC Alloc
12-06 21:17:44.933 4021-4021/? I/art: Alloc sticky concurrent mark sweep GC freed 124(6KB) AllocSpace objects, 0(0B) LOS objects, 12% free, 110MB/126MB, paused 507us total 7.280ms
... //省略幾次GC日志
12-06 21:17:44.988 4021-4021/? W/art: Throwing OutOfMemoryError "Failed to allocate a 100000012 byte allocation with 16777216 free bytes and 81MB until OOM"
該應(yīng)用崩潰的日志打印為:Throwing OutOfMemoryError "Failed to allocate a 100000012 byte allocation with 16777216 free bytes and 81MB until OOM"
,在崩潰前我們可以從日志中看出系統(tǒng)在努力做了幾次GC嘗試鸭廷,但卻無(wú)法釋放足夠內(nèi)存枣抱,最終只能跑出OOM異常。異常信息反應(yīng)出奔潰時(shí)的內(nèi)存狀況辆床,我們結(jié)合到打印的三個(gè)數(shù)據(jù)佳晶,和前面我們所講內(nèi)容,正好能對(duì)上號(hào)讼载。126MB即為total memory轿秧,free memory為16M,所以使用了110M空間咨堤,因堆限制為192M菇篡,堆空閑最少需要預(yù)留512K,所以還剩81M可用一喘,而這81M空間無(wú)法滿足下一次的內(nèi)存分配驱还,所以產(chǎn)生OOM。
圖片處理時(shí)
Android編程中,往往最容易出現(xiàn)OOM的地方就是在圖片處理的時(shí)候议蟆,我們先上個(gè)數(shù)據(jù):一個(gè)像素的顯示需要4字節(jié)(R灼伤、G、B咪鲜、A各占一個(gè)字節(jié)),所以一個(gè)1080x720像素的手機(jī)一個(gè)滿屏幕畫面就需要近3M內(nèi)存撞鹉,而開(kāi)發(fā)一個(gè)輕量應(yīng)用的安裝包大小也差不多就3M左右疟丙,所以說(shuō)圖片很占內(nèi)存。在Android中鸟雏,圖片的資源文件叫做Drawable享郊,存儲(chǔ)在硬盤上,不耗內(nèi)存孝鹊,但我們并無(wú)法對(duì)其進(jìn)行處理炊琉,最多只能進(jìn)行展示。而如果想對(duì)該圖片資源進(jìn)行處理又活,我們需要把這個(gè)Drawable解析為Bitmap形式裝載入內(nèi)存中苔咪。其中Android的不同版本對(duì)Bitmap的存儲(chǔ)方式還有所不同。下面是Android官方文檔中對(duì)此描述的一段話
On Android 2.3.3 (API level 10) and lower, the backing pixel data for a bitmap is stored in native memory. It is separate from the bitmap itself, which is stored in the Dalvik heap. The pixel data in native memory is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash. As of Android 3.0 (API level 11), the pixel data is stored on the Dalvik heap along with the associated bitmap.
bitmap分成兩個(gè)部分柳骄,一部分為bitmap對(duì)象团赏,用以存儲(chǔ)此圖片的長(zhǎng)、寬耐薯、透明度等信息舔清;另一部分為bitmap數(shù)據(jù),用以存儲(chǔ)bitmap的(A)RGB字節(jié)數(shù)據(jù)曲初。在2.3.3及以前版本中bitmap對(duì)象和bitmap數(shù)據(jù)是存儲(chǔ)在不同的內(nèi)存空間上的体谒,bitmap數(shù)據(jù)部分存儲(chǔ)在native內(nèi)存中,GC無(wú)法涉及臼婆。所以之前我們需要調(diào)用bitmap的recycle方法來(lái)顯示的告訴系統(tǒng)此處內(nèi)存可回收抒痒,而在3.0版本開(kāi)始,bitmap的的這兩部分都存儲(chǔ)在了Dalvik堆中颁褂,可以被GC機(jī)制統(tǒng)一處理评汰,也就無(wú)需用recycle了。
關(guān)于bitmap優(yōu)化痢虹,不同版本方法也不想同被去,2.3.3版本及以前,就要做到及時(shí)調(diào)用recycle來(lái)回收不在使用的bitmap奖唯,而3.0開(kāi)始可以使用BitmapFactory.Options.inBitmap
這個(gè)選項(xiàng)惨缆,設(shè)置一個(gè)可復(fù)用的bitmap,這樣以后新的bitmap且大小相同的就可以直接使用這塊內(nèi)存,而無(wú)需重復(fù)申請(qǐng)內(nèi)存坯墨。4.4之后解決了對(duì)大小的限制寂汇,不同大小也可以復(fù)用該塊空間。關(guān)于inBitmap可參考官方文檔捣染。
當(dāng)有多個(gè)bitmap需要顯示時(shí)骄瓣,可以使用LruCache算法。實(shí)踐可以參考一個(gè)Github開(kāi)源庫(kù):DaVinci
作者簡(jiǎn)介
彭濤(@彭濤me) 致力于讓技術(shù)變得易懂且有趣
個(gè)人博客:http://pengtao.me, GitHub地址:https://github.com/CPPAlien