OOM详解

OOM现象:

05:15:04.764: ERROR/dalvikvm-heap(264): 3528000-byte external allocationtoo large for …

05:15:04.764: ERROR/(264): VM won’t let us allocate 3528000 bytes

05:15:04.764: DEBUG/skia(264): — decoder->decode returned false

05:15:04.774: DEBUG/AndroidRuntime(264): Shutting down VM

这几句的意思是,我们的程序申请需要3528000byte太大了,虚拟机不同意给我们,虚拟机shut down自杀了。这个现象,比较常见在需要用到很多图片或者要用很大图片的APP开发中。

OOM(Out Of Memory)错误,什么是OOM?

通俗讲就是当我们的APP需要申请一块内存用来装图片的时候,系统觉得我们的APP所使用的内存已经够多了,不同意给我们的APP更多的内存,即使手机系统里还有1G空余的内存,然后系统抛出OOM,程序弹框shut down。

为什么有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 对单个进程、虚拟机的内存限制。

每个手机的内存限制大小是多少?

ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);

 activityManager.getMemoryClass();

以上方法会返回以M为单位的数字,不同的系统平台或设备上的值都不太一样,比如:HTC G7默认 24M,Galaxy 36M,emulator-2.3 24M,等等。我的moto xt681 是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一般回收的资源是哪些无主的对象内存或者软引用的资源,或者更软的引用资源,比如:

Bitmap bt= BitmapFactory.decodeResource(this.getResources(),R.drawable.splash);

使用bt…//此时的图片资源是强应用,是有主的资源。

bt=null;

此时这个图片资源就是无主的了,gc心情好的时候就会去回收它。

Bitmap bt= BitmapFactory.decodeResource(this.getResources(),R.drawable.splash);

SoftReference< Bitmap > SoftRef=new SoftReference<Bitmap >(bt); 

bt=null;

其他代码….。当程序申请很多内存资源时,gc有可能会释放SoftRef引用的这个图片内存。

bt=SoftRef.get(); 此时可能得到的是null;需要从新加载图片。当然这也说明了用软引用图片资源的好处,就是gc会自动根据需要释放资源,一定程度上避免OOM。

TIPS:编程要养成的习惯,不用的对象置null。其实更好是,不用的图片直接recycle。因为通过置null,让gc来回收,有时候还是会来不及。

For Android specific we should use the 'recycle' method ratherthan 'gc', because 'recycle' will free the memory at the same time, but calling'gc' doesn't guaranty to run and free the memory for same time(if it isnot too critical, we should not call gc in our code) and results canvery every time.
One more thing using 'recycle' is faster than the 'gc' and it improves theperformance.

 

怎么查看APP内存分配情况?

1,通过DDMS中的Heap选项卡监视内存情况:

Heap视图中部有一个叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。

在data object一行中有一列是“TotalSize”,其值就是当前进程中所有Java数据对象的内存总量。

如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落,随着操作次数的增多Total Size的值会越来越大,
  直到到达一个上限后导致进程被kill掉。


 

2,在APP里可以通过Runtime类的totalMemory() ,freeMemory() 两个方法获取VM的一些内存信息,如:

Runtime.getRuntime().freeMemory();

Runtime.getRuntime().totalMemory ();

 

3,adb shell dumpsysmeminfo com.android.demo

 

避免OOM的几个注意点:

1,适当调整图像大小,因为手机屏幕尺寸有限,分配给图像的显示区域有限,尤其对于超大图片,加载自网络或者sd卡,图片文件体积达到几M或者十几M的:

加载到内存前,先算出该bitmap的大小,然后通过适当调节采样率使得加载的图片刚好、或稍大即可在手机屏幕上显示就满意了:

BitmapFactory.Options opts= new BitmapFactory.Options(); 

        opts.inJustDecodeBounds = true; 

        BitmapFactory.decodeFile(imageFile,opts);  //此时不加载实际图片,只获取到图片的宽高,大致可以通过宽度*高度*4来估算图片大小。

        opts.inSampleSize =computeSampleSize(opts, minSideLength, maxNumOfPixels);  // Android提供了一种动态计算的方法computeSampleSize

        opts.inJustDecodeBounds = false; 

        try { 

            return BitmapFactory.decodeFile(imageFile,opts); 

        } catch (OutOfMemoryError err) { 

        } 

2,在ListView或Gallery等控件中一次性加载大量图片时,只加载屏幕显示的资源,尚未显示的不加载,移出屏幕的资源及时释放,可以采用强引用+软引用2级缓存方式,提高加载性能。

3,缓存图像到内存,采用软引用缓存到内存,而不是在每次使用的时候都从新加载到内存;

4,采用低内存占用量的编码方式,比如Bitmap.Config.ARGB_4444比Bitmap.Config.ARGB_8888更省内存;

5,及时回收图像,如果引用了大量Bitmap对象,而应用又不需要同时显示所有图片,可以将暂时用不到的Bitmap对象及时回收掉。对于一些明确知道图片使用情况的场景可以主动recycle。比如:

App的启动splash画面上的图片资源,使用完就recycle;对于帧动画,可以加载一张,画一张,释放一张。

6,不要在循环中创建过多的本地变量; 慎用static,用static来修饰成员变量时,该变量就属于该类,而不是该类的实例,它的生命周期是很长的。如果用它来引用一些资源耗费过多的实例,这时就要谨慎对待了。

public class ClassName {  

     private static Context mContext;  

     //省略  

}  

如果将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;

// Hard cache, with a fixed maximum capacity and a lifeduration

privatestaticfinalHashMap<String, Bitmap>sHardBitmapCache = newLinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY, 0.75f, true) {

privatestaticfinallongserialVersionUID = -57738079457331894L;

@Override

protectedbooleanremoveEldestEntry(LinkedHashMap.Entry<String,Bitmap> eldest) {

if(size() > HARD_CACHE_CAPACITY) {

sSoftBitmapCache.put(eldest.getKey(), newSoftReference<Bitmap>(eldest.getValue()));

returntrue;

} else

returnfalse;

}

};

// Soft cache for bitmap kicked out of hard cache

privatefinalstaticConcurrentHashMap<String,SoftReference<Bitmap>> sSoftBitmapCache = newConcurrentHashMap<String,SoftReference<Bitmap>>(HARD_CACHE_CAPACITY);

publicBitmap getBitmap(String id) {

// First try the hard reference cache

synchronized(sHardBitmapCache) {

finalBitmap bitmap = sHardBitmapCache.get(id);

if(bitmap != null) {

// Bitmap found in hard cache

// Move element to first position, so that it is removedlast

sHardBitmapCache.remove(id);

sHardBitmapCache.put(id, bitmap);

returnbitmap;

} else{

// Then try the soft reference cache

SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(id);

if(bitmapReference != null) {

finalBitmap bitmap = bitmapReference.get();

if(bitmap != null) {

// Bitmap found in soft cache

returnbitmap;

} else{

// Soft reference has been Garbage Collected

sSoftBitmapCache.remove(id);

}

}

}

}

returnnull;

}

publicvoidputBitmap(Stringid, Bitmap bitmap) {

synchronized(sHardBitmapCache) {

if(sHardBitmapCache != null) {

sHardBitmapCache.put(id, bitmap);

}

}

}

 

4,LruCache +sd的缓存方式

LruCache 类特别合适用来caching bitmaps;

private LruCachemMemoryCache;

@Override

protected voidonCreate(Bundle savedInstanceState) {    ...

    // Get memory class of this device,exceeding this amount will throw an

    // OutOfMemory exception.

    final int memClass = ((ActivityManager)context.getSystemService(

           Context.ACTIVITY_SERVICE)).getMemoryClass(); 

    // Use 1/8th of the available memory forthis memory cache.

    final int cacheSize = 1024 * 1024 *memClass / 8; 

    mMemoryCache = new LruCache(cacheSize) {

        @Override

        protected int sizeOf(String key, Bitmapbitmap) {

            // The cache size will be measuredin bytes rather than number of items.

            return bitmap.getByteCount();

        }

    };

    ...

public void addBitmapToMemoryCache(Stringkey, Bitmap bitmap) {

    if (getBitmapFromMemCache(key) == null) {

        mMemoryCache.put(key, bitmap);

    }

public BitmapgetBitmapFromMemCache(String key) {

    return mMemoryCache.get(key);

}

当加载位图到ImageView时,LruCache会先被检查是否存在这张图片。如果找到有,它会被用来立即更新 ImageView 组件,否则一个后台线程则被触发去处理这张图片。

public void loadBitmap(intresId, ImageView imageView) {

    final String imageKey =String.valueOf(resId); 

    final Bitmap bitmap =getBitmapFromMemCache(imageKey);

    if (bitmap != null) {

        mImageView.setImageBitmap(bitmap);

    } else {        mImageView.setImageResource(R.drawable.image_placeholder); //默认图片

        BitmapWorkerTask task = newBitmapWorkerTask(mImageView);

        task.execute(resId);

    }

}

上面的程序中 BitmapWorkerTask 也需要做添加到内存Cache中的动作:

class BitmapWorkerTaskextends AsyncTask {

    ...

    // Decode image in background.

    @Override

    protected Bitmap doInBackground(Integer...params) {

        final Bitmap bitmap =decodeSampledBitmapFromResource( 

                getResources(),params[0], 100, 100)); 

        addBitmapToMemoryCache(String.valueOf(params[0]),bitmap); 

        returnbitmap; 

    }

    ...

}

Use a Disk Cache [使用磁盘缓存]

private DiskLruCachemDiskCache; 

private static final intDISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB 

private static finalString DISK_CACHE_SUBDIR = "thumbnails"; 

 @Override 

protected voidonCreate(Bundle savedInstanceState) { 

    ... 

    //Initialize memory cache 

    ... 

    FilecacheDir = getCacheDir(this, DISK_CACHE_SUBDIR); 

    mDiskCache= DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE); 

    ... 

}  

class BitmapWorkerTaskextends AsyncTask { 

    ... 

    //Decode image in background. 

    @Override 

    protectedBitmap doInBackground(Integer... params) { 

        finalString imageKey = String.valueOf(params[0]);  

        //Check disk cache in background thread 

        Bitmapbitmap = getBitmapFromDiskCache(imageKey);  

        if(bitmap == null) { // Not found in disk cache 

            //Process as normal 

            finalBitmap bitmap = decodeSampledBitmapFromResource( 

                    getResources(),params[0], 100, 100)); 

        }  

        //Add final bitmap to caches 

        addBitmapToCache(String.valueOf(imageKey,bitmap);  

        returnbitmap; 

    } 

    ... 

}  

public voidaddBitmapToCache(String key, Bitmap bitmap) { 

    //Add to memory cache as before 

    if(getBitmapFromMemCache(key) == null) { 

        mMemoryCache.put(key,bitmap); 

    }  

    //Also add to disk cache 

    if(!mDiskCache.containsKey(key)) { 

        mDiskCache.put(key,bitmap); 

    } 

}  

public BitmapgetBitmapFromDiskCache(String key) { 

    returnmDiskCache.get(key); 

}  

// Creates a uniquesubdirectory of the designated app cache directory. Tries to use external 

// but if not mounted,falls back on internal storage. 

public static FilegetCacheDir(Context context, String uniqueName) { 

    //Check if media is mounted or storage is built-in, if so, try and use externalcache dir 

    //otherwise use internal cache dir 

    finalString cachePath = Environment.getExternalStorageState() ==Environment.MEDIA_MOUNTED 

            ||!Environment.isExternalStorageRemovable() ? 

                    context.getExternalCacheDir().getPath(): context.getCacheDir().getPath();  

    returnnew File(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或新建/删除)分配内存,这样的分配是不计入对24 MB的限制。这是真的,从本机代码分配内存是为从Java方便,但它可以被用来存储在RAM中的数据(即使图像数据)的一些大金额。

2,使用OpenGL的纹理-纹理内存不计入限制 ,要查看您的应用程序确实分配多少内存可以使用android.os.Debug.getNativeHeapAllocatedSize( ),可以使用上面介绍的两种技术的Nexus之一,我可以轻松地为一个单一的前台进程分配300MB - 10倍以上的默认24 MB的限制,从上面来看使用navtive代码分配内存是不在24MB的限制内的(开放的GL的质地也是使用navtive代码分配内存的)。

但是,这2个方法有个风险就是,本地堆分配内存超过系统可用内存限制的话,通常都是直接崩溃。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值