OOM常见问题与分析
OOM现象:
05:15:04.764:ERROR/dalvikvm-heap(264):3528000-byteexternalallocationtoolargefor…
05:15:04.764:ERROR/(264):VMwon’tletusallocate3528000bytes
05:15:04.764:DEBUG/skia(264):—decoder->decodereturnedfalse
05:15:04.774:DEBUG/AndroidRuntime(264):ShuttingdownVM
这几句的意思是,我们的程序申请需要3528000byte太大了,虚拟机不同意给我们,虚拟机shutdown自杀了。这个现象,比较常见在需要用到很多图片或者要用很大图片的APP开发中。
OOM(OutOfMemory)错误,什么是OOM?
通俗讲就是当我们的APP需要申请一块内存用来装图片的时候,系统觉得我们的APP所使用的内存已经够多了,不同意给我们的APP更多的内存,即使手机系统里还有1G空余的内存,然后系统抛出OOM,程序弹框shutdown。
为什么有OOM,OOM的必然性!
因为android系统app的每个进程或者每个虚拟机有个最大内存限制,如果申请的内存资源超过了这个限制,系统就会抛出OOM错误。跟整个设备的剩余内存没太大关系。比如比较早的android系统一个虚拟机最多16M内存,当一个app启动后,虚拟机不停的申请内存资源用来装载图片,当超过内存上限时就OOM。
Android系统APP内存限制怎么确定的?
Android的APP内存组成:
APP内存由dalvik内存和native内存2部分组成,dalvik也就是java堆,创建的对象就是在这里分配的,而native是通过c/c++方式申请的内存,Bitmap就是以这种方式分配的(android3.0以后,系统都默认是通过dalvik分配的,native作为堆来管理)。这2部分加起来不能超过android对单个进程、虚拟机的内存限制。
每个手机的内存限制大小是多少?
ActivityManageractivityManager=(ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
activityManager.getMemoryClass();
以上方法会返回以M为单位的数字,不同的系统平台或设备上的值都不太一样,比如:HTCG7默认24M,Galaxy36M,emulator-2.324M,等等。我的motoxt681是42M。3.0系统的设备默认是48M。
上面取到是虚拟机的最大内存资源。而对于heap堆的大小限制,可以查看/system/build.prop文件。
dalvik.vm.heapstartsize=5m
dalvik.vm.heapgrowthlimit=48m
dalvik.vm.heapsize=256m
heapsize参数表示单个进程heap可用的最大内存,但如果存在如下参数:
dalvik.vm.heapgrowthlimit=48m表示单个进程heap内存被限定在48m,即程序运行过程中实际只能使用48m内存。
为什么android系统设定APP的内存限制?
1,要使开发者内存使用更为合理。限制每个应用的可用内存上限,可以防止某些应用程序恶意或者无意使用过多的内存,而导致其他应用无法正常运行。Android是有多进程的,如果一个进程(也就是一个应用)耗费过多的内存,其他的应用无法运行了。因为有了限制,使得开发者必须好好利用有限的资源,优化资源的使用。
2,即使有万千图片、千万数据需要使用到,但是特定时刻需要展示给用户看的总是有限,因为设备的屏幕显示就那么大,上面可以放的信息就是很有限的。大部分信息都是出于准备显示状态,所以没必要给予太多heap内存。也就是出现OOM现象,绝大部分原因是我们的程序设计上有问题,需要优化。比如可以通过时间换空间,不停的加载要用的图片,不停的回收不用的图片,把大图片解析到适合手机屏幕大小的图片等。
3,android上的APP使用独立虚拟机,每开一个应用就会打开至少一个独立的虚拟机。这样可以避免虚拟机崩溃导致整个系统崩溃,同时代价就是需要浪费更多内存。这些设计确保了android的稳定性。
不是android有gc会自动回收资源么,为什么还会OOM?
Android不是用gc会自动回收资源么,为什么app的哪些不用的资源不回收呢?
Android的gc会按照特定的算法回收程序不用的内存资源,避免app的内存申请越积越多。但是Gc一般回收的资源是哪些无主的对象内存或者软引用的资源,或者更软的引用资源,比如:
Bitmapbt=BitmapFactory.decodeResource(this.getResources(),R.drawable.splash);
使用bt…//此时的图片资源是强应用,是有主的资源。
bt=null;
此时这个图片资源就是无主的了,gc心情好的时候就会去回收它。
Bitmapbt=BitmapFactory.decodeResource(this.getResources(),R.drawable.splash);
SoftReferenceitmap>SoftRef=newSoftReferenceitmap>(bt);
bt=null;
其他代码….。当程序申请很多内存资源时,gc有可能会释放SoftRef引用的这个图片内存。
bt=SoftRef.get();此时可能得到的是null;需要从新加载图片。当然这也说明了用软引用图片资源的好处,就是gc会自动根据需要释放资源,一定程度上避免OOM。
TIPS:编程要养成的习惯,不用的对象置null。其实更好是,不用的图片直接recycle。因为通过置null,让gc来回收,有时候还是会来不及。
ForAndroidspecificweshouldusethe'recycle'methodratherthan'gc',because'recycle'willfreethememoryatthesametime,butcalling'gc'doesn'tguarantytorunandfreethememoryforsametime(ifitisnottoocritical,weshouldnotcallgcinourcode)andresultscanveryeverytime.
Onemorethingusing'recycle'isfasterthanthe'gc'anditimprovestheperformance.
怎么查看APP内存分配情况?
1,通过DDMS中的Heap选项卡监视内存情况:
Heap视图中部有一个叫做dataobject,即数据对象,也就是我们的程序中大量存在的类类型的对象。
在dataobject一行中有一列是“TotalSize”,其值就是当前进程中所有Java数据对象的内存总量。
如果代码中存在没有释放对象引用的情况,则dataobject的TotalSize值在每次GC后不会有明显的回落,随着操作次数的增多TotalSize的值会越来越大,
直到到达一个上限后导致进程被kill掉。
2,在APP里可以通过Runtime类的totalMemory(),freeMemory()两个方法获取VM的一些内存信息,如:
Runtime.getRuntime().freeMemory();
Runtime.getRuntime().totalMemory();
3,adbshelldumpsysmeminfocom.android.demo
避免OOM的几个注意点:
1,适当调整图像大小,因为手机屏幕尺寸有限,分配给图像的显示区域有限,尤其对于超大图片,加载自网络或者sd卡,图片文件体积达到几M或者十几M的:
加载到内存前,先算出该bitmap的大小,然后通过适当调节采样率使得加载的图片刚好、或稍大即可在手机屏幕上显示就满意了:
BitmapFactory.Optionsopts=newBitmapFactory.Options();
opts.inJustDecodeBounds=true;
BitmapFactory.decodeFile(imageFile,opts);//此时不加载实际图片,只获取到图片的宽高,大致可以通过宽度*高度*4来估算图片大小。
opts.inSampleSize=computeSampleSize(opts,minSideLength,maxNumOfPixels);//Android提供了一种动态计算的方法computeSampleSize
opts.inJustDecodeBounds=false;
try{
returnBitmapFactory.decodeFile(imageFile,opts);
}catch(OutOfMemoryErrorerr){
}
2,在ListView或Gallery等控件中一次性加载大量图片时,只加载屏幕显示的资源,尚未显示的不加载,移出屏幕的资源及时释放,可以采用强引用+软引用2级缓存方式,提高加载性能。
3,缓存图像到内存,采用软引用缓存到内存,而不是在每次使用的时候都从新加载到内存;
4,采用低内存占用量的编码方式,比如Bitmap.Config.ARGB_4444比Bitmap.Config.ARGB_8888更省内存;
5,及时回收图像,如果引用了大量Bitmap对象,而应用又不需要同时显示所有图片,可以将暂时用不到的Bitmap对象及时回收掉。对于一些明确知道图片使用情况的场景可以主动recycle。比如:
App的启动splash画面上的图片资源,使用完就recycle;对于帧动画,可以加载一张,画一张,释放一张。
6,不要在循环中创建过多的本地变量;慎用static,用static来修饰成员变量时,该变量就属于该类,而不是该类的实例,它的生命周期是很长的。如果用它来引用一些资源耗费过多的实例,这时就要谨慎对待了。
publicclassClassName{
privatestaticContextmContext;
//省略
}
如果将Activity赋值到mContext的话。即使该Activity已经onDestroy,由于仍有对象保存它的引用,因此该Activity依然不会被释放。
7,自定义堆内存分配大小,优化Dalvik虚拟机的堆内存分配;
App避免OOM的几种方式
1,直接null或recycle。
对于app里使用的大量图片,采用方式:使用时加载,不显示时直接置null或recycle。
这样处理是个好习惯,基本上可以杜绝OOM。但是缺憾是代码多了,可能会忘记某些资源recycle。而且有些情况下会出现特定的图片反复加载、释放、再加载等,低效率的事情。
2,简单通过SoftReference引用方式管理图片资源
建个SoftReference的hashmap;
使用图片时先查询这个hashmap是否有SoftReference,SoftReference里的图片是否空;
如果空就加载图片到SoftReference并加入hashmap。
无需在代码里显式的处理图片的回收和释放,gc会自动处理资源的释放。
这种方式处理起来简单实用,能一定程度上避免前一种方法反复加载释放的低效率。但还不够优化。
3,强引用+软引用二级缓
Android示范程序ImageDownloader.java,使用了一个二级缓存机制。就是有一个数据结构中直接持有解码成功的Bitmap对象引用,同时使用一个二级缓存数据结构保持淘汰的Bitmap对象的SoftReference对象,由于SoftReference对象的特殊性,系统会在需要内存的时候首先将SoftReference对象持有的对象释放掉,也就是说当VM发现可用内存比较少了需要触发GC的时候,就会优先将二级缓存中的Bitmap回收,而保有一级缓存中的Bitmap对象用于显示。
其实这个解决方案最为关键的一点是使用了一个比较合适的数据结构,那就是LinkedHashMap类型来进行一级缓存Bitmap的容器,由于LinkedHashMap的特殊性,我们可以控制其内部存储对象的个数并且将不再使用的对象从容器中移除,放到SoftReference二级缓存里,我们可以在一级缓存中一直保存最近被访问到的Bitmap对象,而已经被访问过的图片在LinkedHashMap的容量超过我们预设值时将会把容器中存在时间最长的对象移除,这个时候我们可以将被移除出LinkedHashMap中的对象存放至二级缓存容器中,而二级缓存中对象的管理就交给系统来做了,当系统需要GC时就会首先回收二级缓存容器中的Bitmap对象了。
在获取图片对象的时候先从一级缓存容器中查找,如果有对应对象并可用直接返回,如果没有的话从二级缓存中查找对应的SoftReference对象,判断SoftReference对象持有的Bitmap是否可用,可用直接返回,否则返回空。如果2级缓存都找不到图片,就直接加载图片资源。
privatestaticfinalintHARD_CACHE_CAPACITY=16;
//Hardcache,withafixedmaximumcapacityandalifeduration
privatestaticfinalHashMap
privatestaticfinallongserialVersionUID=-57738079457331894L;
@Override
protectedbooleanremoveEldestEntry(LinkedHashMap.Entry
if(size()>HARD_CACHE_CAPACITY){
sSoftBitmapCache.put(eldest.getKey(),newSoftReference
returntrue;
}else
returnfalse;
}
};
//Softcacheforbitmapkickedoutofhardcache
privatefinalstaticConcurrentHashMap
publicBitmapgetBitmap(Stringid){
//Firsttrythehardreferencecache
synchronized(sHardBitmapCache){
finalBitmapbitmap=sHardBitmapCache.get(id);
if(bitmap!=null){
//Bitmapfoundinhardcache
//Moveelementtofirstposition,sothatitisremovedlast
sHardBitmapCache.remove(id);
sHardBitmapCache.put(id,bitmap);
returnbitmap;
}else{
//Thentrythesoftreferencecache
SoftReference
if(bitmapReference!=null){
finalBitmapbitmap=bitmapReference.get();
if(bitmap!=null){
//Bitmapfoundinsoftcache
returnbitmap;
}else{
//SoftreferencehasbeenGarbageCollected
sSoftBitmapCache.remove(id);
}
}
}
}
returnnull;
}
publicvoidputBitmap(Stringid,Bitmapbitmap){
synchronized(sHardBitmapCache){
if(sHardBitmapCache!=null){
sHardBitmapCache.put(id,bitmap);
}
}
}
4,LruCache+sd的缓存方式
LruCache类特别合适用来cachingbitmaps;
privateLruCachemMemoryCache;
@Override
protectedvoidonCreate(BundlesavedInstanceState){...
//Getmemoryclassofthisdevice,exceedingthisamountwillthrowan
//OutOfMemoryexception.
finalintmemClass=((ActivityManager)context.getSystemService(
Context.ACTIVITY_SERVICE)).getMemoryClass();
//Use1/8thoftheavailablememoryforthismemorycache.
finalintcacheSize=1024*1024*memClass/8;
mMemoryCache=newLruCache(cacheSize){
@Override
protectedintsizeOf(Stringkey,Bitmapbitmap){
//Thecachesizewillbemeasuredinbytesratherthannumberofitems.
returnbitmap.getByteCount();
}
};
...
}
publicvoidaddBitmapToMemoryCache(Stringkey,Bitmapbitmap){
if(getBitmapFromMemCache(key)==null){
mMemoryCache.put(key,bitmap);
}
}
publicBitmapgetBitmapFromMemCache(Stringkey){
returnmMemoryCache.get(key);
}
当加载位图到ImageView时,LruCache会先被检查是否存在这张图片。如果找到有,它会被用来立即更新ImageView组件,否则一个后台线程则被触发去处理这张图片。
publicvoidloadBitmap(intresId,ImageViewimageView){
finalStringimageKey=String.valueOf(resId);
finalBitmapbitmap=getBitmapFromMemCache(imageKey);
if(bitmap!=null){
mImageView.setImageBitmap(bitmap);
}else{mImageView.setImageResource(R.drawable.image_placeholder);//默认图片
BitmapWorkerTasktask=newBitmapWorkerTask(mImageView);
task.execute(resId);
}
}
上面的程序中BitmapWorkerTask也需要做添加到内存Cache中的动作:
classBitmapWorkerTaskextendsAsyncTask{
...
//Decodeimageinbackground.
@Override
protectedBitmapdoInBackground(Integer...params){
finalBitmapbitmap=decodeSampledBitmapFromResource(
getResources(),params[0],100,100));
addBitmapToMemoryCache(String.valueOf(params[0]),bitmap);
returnbitmap;
}
...
}
UseaDiskCache[使用磁盘缓存]
privateDiskLruCachemDiskCache;
privatestaticfinalintDISK_CACHE_SIZE=1024*1024*10;//10MB
privatestaticfinalStringDISK_CACHE_SUBDIR="thumbnails";
@Override
protectedvoidonCreate(BundlesavedInstanceState){
...
//Initializememorycache
...
FilecacheDir=getCacheDir(this,DISK_CACHE_SUBDIR);
mDiskCache=DiskLruCache.openCache(this,cacheDir,DISK_CACHE_SIZE);
...
}
classBitmapWorkerTaskextendsAsyncTask{
...
//Decodeimageinbackground.
@Override
protectedBitmapdoInBackground(Integer...params){
finalStringimageKey=String.valueOf(params[0]);
//Checkdiskcacheinbackgroundthread
Bitmapbitmap=getBitmapFromDiskCache(imageKey);
if(bitmap==null){//Notfoundindiskcache
//Processasnormal
finalBitmapbitmap=decodeSampledBitmapFromResource(
getResources(),params[0],100,100));
}
//Addfinalbitmaptocaches
addBitmapToCache(String.valueOf(imageKey,bitmap);
returnbitmap;
}
...
}
publicvoidaddBitmapToCache(Stringkey,Bitmapbitmap){
//Addtomemorycacheasbefore
if(getBitmapFromMemCache(key)==null){
mMemoryCache.put(key,bitmap);
}
//Alsoaddtodiskcache
if(!mDiskCache.containsKey(key)){
mDiskCache.put(key,bitmap);
}
}
publicBitmapgetBitmapFromDiskCache(Stringkey){
returnmDiskCache.get(key);
}
//Createsauniquesubdirectoryofthedesignatedappcachedirectory.Triestouseexternal
//butifnotmounted,fallsbackoninternalstorage.
publicstaticFilegetCacheDir(Contextcontext,StringuniqueName){
//Checkifmediaismountedorstorageisbuilt-in,ifso,tryanduseexternalcachedir
//otherwiseuseinternalcachedir
finalStringcachePath=Environment.getExternalStorageState()==Environment.MEDIA_MOUNTED
||!Environment.isExternalStorageRemovable()?
context.getExternalCacheDir().getPath():context.getCacheDir().getPath();
returnnewFile(cachePath+File.separator+uniqueName);
}
两种场景下的图片加载建议:
1,网络下载大量图片
比如微博客户端:
多线程异步网络下载图片,小图直接用LRUcache+softref+sd卡,大图按需下载;
2,对于需要展现非常多条目信息的listview、gridview等的情况
在adapter的getview函数里有个convertView参数,告知你是否有课利旧的view对象。如果不使用利旧convertView的话,每次调用getView时每次都会重新创建View,这样之前的View可能还没有销毁,加之不断的新建View势必会造成内存泄露。同时利旧convertView时,里面原有的图片等资源就会变成无主的了。
推介使用convertView+静态类ViewHolder。
在这里,官方给出了解释
提升Adapter的两种方法
重用缓存convertView传递给getView()方法来避免填充不必要的视图
使用ViewHolder模式来避免没有必要的调用findViewById():因为太多的findViewById也会影响性能
ViewHolder类的作用
ViewHolder模式通过getView()方法返回的视图的标签(Tag)中存储一个数据结构,这个数据结构包含了指向我们
要绑定数据的视图的引用,从而避免每次调用getView()的时候调用findViewById())
远远超过限制的内存分配方式有两种:
1,是从本机代码分配内存。使用NDK(本地开发工具包)和JNI,它可能从C级(如的malloc/free或新建/删除)分配内存,这样的分配是不计入对24MB的限制。这是真的,从本机代码分配内存是为从Java方便,但它可以被用来存储在RAM中的数据(即使图像数据)的一些大金额。
2,使用OpenGL的纹理-纹理内存不计入限制,要查看您的应用程序确实分配多少内存可以使用android.os.Debug.getNativeHeapAllocatedSize(),可以使用上面介绍的两种技术的Nexus之一,我可以轻松地为一个单一的前台进程分配300MB-10倍以上的默认24MB的限制,从上面来看使用navtive代码分配内存是不在24MB的限制内的(开放的GL的质地也是使用navtive代码分配内存的)。
但是,这2个方法有个风险就是,本地堆分配内存超过系统可用内存限制的话,通常都是直接崩溃。