分析几种预防OOM的方法

一、OOM介绍

1、VM运行时内存分析

JVM执行Java程序的过程中,会使用到各种数据区域,这些区域有各自的用途、创建和销毁时间。JVM包括下列几个运行时数据区域:

  • 1.程序计数器(Program Counter Register):
    此内存区域是唯一一个在VM Spec中没有规定任何OutOfMemoryError情况的区域。

  • 2.Java虚拟机栈(Java Virtual Machine Stacks):
    在VM Spec中对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果VM栈可以动态扩展(VM Spec中允许固定长度的VM栈),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

  • 3.本地方法栈(Native Method Stacks):
    和VM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError异常。

  • 4.Java堆(Java Heap)
    对于绝大多数应用来说,Java堆是虚拟机管理最大的一块内存。Java堆是被所有线程共享的,在虚拟机启动时创建。Java堆的唯一目的就是存放对象实例(以及数组),绝大部分的对象实例都在这里分配。
    Java堆内还有更细致的划分:新生代、老年代,再细致一点的:eden、from survivor、to survivor,甚至更细粒度的本地线程分配缓冲(TLAB)等。
    根据VM Spec的要求,Java堆可以处于物理上不连续的内存空间,它逻辑上是连续的即可,就像我们的磁盘空间一样。实现时可以选择实现成固定大小的,也可以是可扩展的,不过当前所有商业的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

  • 5.方法区(Method Area)

  • 6.运行时常量池(Runtime Constant Pool):
    运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法在申请到内存时会抛出OutOfMemoryError异常。

2、OOM出现原因

  • 已知前提
    Android的应用程序所能申请的最大内存都是有限的
  • 定义
    OOM是指APP向系统申请内存的请求超过了应用所能有的最大阀值的内存,系统无法再分配多余的空间,就会造成OOM
  • 出现原因
    1、持续发生了内存泄漏(Memory Leak),累积到一定程度导致OOM
    持续发生了内存泄漏(Memory Leak),累积到一定程度导致OOM;
    2、一次性申请很多内存(比如说一次创建大的数组或者是载入大的文件如图片的时候)
  • 造成结果
    发生oom错误,进程被强制kill掉,kill掉的进程内存会被系统回收

二、StrictMode介绍

StrictMode:严苛模式,主要用来检测程序中几种违例情况的Android原生开发者工具。 主要分为ThreadPolicy(线程检测策略)跟VmPolicy(虚拟机检测策略)两类检测内容:

1、ThreadPolicy

  • detectDiskReads()–主线程读取文件
  • detectDiskWrites()–主线程写文件
  • detectNetwork()–主线程网络操作
  • detectCustomSlowCalls()–自定义耗时操作

2、VmPolicy

  • detectLeakedSqlLiteObjects–Sqlite对象泄漏
  • detectActivityLeaks–Activity泄漏
  • detectLeakedClosableObjects–未关闭的Closable对象泄露
  • detectLeakedRegistrationObjects–广播注册后没取消注册

3、集成方式

可以按照不同场景需求选择检测不同违规操作。推荐在Application onCreate()方法中集成:

if (BuildConfig.DEBUG) {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()//线程策略(ThreadPolicy)
            .detectDiskReads()//检测在UI线程读磁盘操作
            .detectDiskWrites()//检测UI线程写磁盘操作
            .detectCustomSlowCalls()//发现UI线程调用的哪些方法执行得比较慢
            .detectResourceMismatches()//最低版本为API23  发现资源不匹配
            .detectNetwork() //检测在UI线程执行网络操作
            .penaltyDialog()//一旦检测到弹出Dialog
            .penaltyDropBox()//一旦检测到将信息存到DropBox文件夹中 data/system/dropbox
            .penaltyLog()//一旦检测到将信息以LogCat的形式打印出来
            .permitDiskReads()//允许UI线程在磁盘上读操作
            .build());

    StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()//虚拟机策略(VmPolicy)
            .detectActivityLeaks()//最低版本API11 用户检查 Activity 的内存泄露情况
            .detectCleartextNetwork()//最低版本为API23  检测明文的网络
            .detectFileUriExposure()//最低版本为API18   检测file://或者是content://
            .detectLeakedClosableObjects()//最低版本API11  资源没有正确关闭时触发
            .detectLeakedRegistrationObjects()//最低版本API16  BroadcastReceiver、ServiceConnection是否被释放
            .detectLeakedSqlLiteObjects()//最低版本API9   资源没有正确关闭时回触发
            .setClassInstanceLimit(TestLeakActivity.class, 2)//设置某个类的同时处于内存中的实例上限,可以协助检查内存泄露
            .penaltyLog()
            .build());

}

4、集成StrictMode后代码违规表现

由上面StrictMode API可知,检测到违规代码时,我们可以选择以应用崩溃/弹框/日志输出等形式表现出来。一般推荐输出到日志即可。下面简单列举下执行到代码中违规操作时日志表现:

  • I/O流未关闭
2019-05-06 19:51:25.523 22941-22951/com.whh.strictmode E/StrictMode: A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks.
    java.lang.Throwable: Explicit termination method 'close' not called
        at dalvik.system.CloseGuard.open(CloseGuard.java:180)
        at java.io.FileOutputStream.(FileOutputStream.java:222)
        at java.io.FileOutputStream.(FileOutputStream.java:169)
        at com.whh.strictmode.MainActivity.testReadWrite(MainActivity.java:83)
        at com.whh.strictmode.MainActivity.onClick(MainActivity.java:53)
  • Activity泄漏
2019-05-06 19:52:48.467 23148-23148/com.whh.strictmode E/WindowManager: android.view.WindowLeaked: Activity com.whh.strictmode.leakactivity.TestLeakActivity has leaked window DecorView@86a60b[] that was originally added here
        at android.view.ViewRootImpl.(ViewRootImpl.java:418)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:331)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
        at android.app.Dialog.show(Dialog.java:322)
        at com.whh.strictmode.leakactivity.TestLeakActivity.onCreate(TestLeakActivity.java:38)
        
2019-05-06 19:53:18.916 23148-23148/com.whh.strictmode E/StrictMode: class com.whh.strictmode.leakactivity.TestLeakActivity; instances=2; limit=1
    android.os.StrictMode$InstanceCountViolation: class com.whh.strictmode.leakactivity.TestLeakActivity; instances=2; limit=1
        at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
  • Bitmap泄漏
Caused by: java.lang.OutOfMemoryError: Failed to allocate a 51916812 byte allocation with 4188384 free bytes and 24MB until OOM
        at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
        at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
        at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:620)
        at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:660)
        at com.whh.strictmode.MainActivity.testBitmap(MainActivity.java:105)
        at com.whh.strictmode.MainActivity.onClick(MainActivity.java:53)

三、如何避免OOM

1、I/O流用完及时关闭

推荐用try-with-resource语句–更优雅的关闭资源。
(JDK7及其之后的资源关闭方式)
try-with-resource并不是JVM虚拟机的新增功能,只是JDK实现了一个语法糖,实际实现原理还是try-catch-finally。
以前用try-catch-finally使用I/O流代码如下:

//try-catch外部定义I/O流
InputStream is = null;
OutputStream os = null;
try {
    //try括号中I/O赋值
    is = new FileInputStream(src);
    os = new FileOutputStream(dst);
    //...进行一些I/O操作
} catch (Exception e) {
} finally {
    //finally中关闭I/O流
    try {
        if (is != null) {
            is.close();
        }
    } catch (IOException e) {
    }
    try {
        if (os != null) {
            os.close();
        }
    } catch (IOException e) {
    }
}

这种方式代码繁杂不说,有时候还容易忘记关闭输入输出流。而换成try-with-resource语句后,代码如下:

try (InputStream is = new FileInputStream(src);
     OutputStream os = new FileOutputStream(dst)) {
    //...进行一些I/O操作
} catch (Exception e) {
}

这种方式优雅简洁,而且不用担心忘记关闭输入输出流,因为try()括号中的I/O流会自动关闭

2、确保Activity实例引用不会被其他类长期持有

  • 1、Activity中非静态内部类会持有Activity实例引用:Java的非静态内部类在构造的时候,会将外部类的引用传递进来,并且作为内部类的一个属性,因此,内部类会隐式地持有其外部类的引用。
    所以,定义内部类时一定要注意看当前类是否持有Activity引用或者当前类是否是Activity
  • 2、将Activity当成Context传入其他类,会导致其他类持有Activity实例引用。
    所以,一般情况下要用Context时最好用ApplicationContext,必须用ActivityContext时要注意Context引用回收。
  • 3、将Activity中View注入其他类时,由于View持有ActivityContext引用,也会导致其他类持有Activity实例引用。
    所以,将View注入其他类时,确保该View引用在用完后被回收
  • 4、Activity实现了某些接口,作为观察者被注册到其他类时,也会导致Activity引用被其他类持有。
    所以,Activity中注册监听与反注册最好成对出现
  • 5、Activity生命周期内注册广播时,要在对应生命周期中取消广播注册。

3、图片加载

  • 1、尽量使用图片加载框架(ImageLoader/Picasso/Glide…)加载图片,因为使用图片加载框架不需要去考虑图片Bitmap回收问题等,而且0图片加载框架基本都支持扩展(比如图片转换等)跟自定义(比如图片缓存大小、位置等)。
  • 2、不用框架加载图片时,请用LruCache管理图片缓存列表。
  • 3、如果以上两种方式都不用,一定要确保Bitmap使用完后合理回收。
  • 4、加载图片时,一定要考虑到图片尺寸压缩/颜色质量,根据需要保证图片缓存的合理大小跟颜色质量。由于Android加载图片基本使用BitmapFactory加载,这里简单介绍下两种图片利用技巧:
//1、利用inSampleSize进行图片采样
BitmapFactory.Options options = new BitmapFactory.Options();
//inSampleSize越大,压缩率越大。inSampleSize==1时表示原图
//当确定图片大小跟需要的图片大小时,可以计算出采样率进行图片压缩
options.inSampleSize = 2;
Bitmap bmp = BitmapFactory.decodeFile(path, options);
//2、利用inSampleSize进行图片采样
BitmapFactory.Options options = new BitmapFactory.Options();
//当对图片透明度需求不高时,可以将图片颜色质量设为RGB_565
options.inPreferredConfig = Config.RGB_565;
Bitmap bmp = BitmapFactory.decodeFile(path, options);
//3、利用inBitmap复用旧的Bitmap的内存,而不用重新分配以及销毁旧Bitmap,进而改善运行效率
BitmapFactory.Options options = new BitmapFactory.Options();
//mOldBitmap是一个旧的Bitmap,必须是mutable的(支持修改).新的bmp直接复用该mOldBitmap的内存空间
//使用条件是mOldBitmap占用的空间必须大于等于新bmp占用的内存空间
options.inBitmap = mOldBitmap;
Bitmap bmp = BitmapFactory.decodeFile(path, options);

你可能感兴趣的:(android)