OutOfMemoryError可以被try-catch吗

偶尔在Android中有看到有一段捕获OutOfMemoryError的代码 (View的buildDrawingCacheImpl方法),不禁想到难道OutOfMemoryError也能被try-catch?其实还真的可以,但是只有在特定场景下捕获OOM才是有意义的,下面主要来看看Java抛出OOM的场景以及何时可以捕获OOM。

Java Heap发生OOM

Java堆用于存储对象实例,不断创建对象,并且GC没有及时回收这些对象的时候,就会发生OOM:

1-Xmx30m

上面先指定了 JavaHeap 大小为30Mb,再进行测试:

 1import java.util.ArrayList;
 2
 3public class HeapOOM {
 4    public static void main(String[] args) {
 5        new HeapOOM().testOOM();
 6    }
 7
 8    public void testOOM() {
 9        ArrayList<OOMClass> list = new ArrayList<>();
10        while (true){
11            list.add(new OOMClass());
12        }
13    }
14
15    static class OOMClass { }
16}

Java Heap溢出最常见,通常遇到此类情况需要分析Java Heap的转储快照,首先要分析出的就是到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。如果是内存泄漏,可以通过查看GC Roots引用链来定位内存泄露的位置:

例如第一次产生OOM时生成Head快照文件,通过VisualVM进行堆快照分析(指定生成快照及快照文件名):

1 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heapdump.hprof

如果是内存溢出,那么要么从代码层面优化,尽量避免对象的生命周期过长,或者尽量复用对象,如果物理内存够用,那么尽量增大Java Heap的大小也是OK的。

虚拟机栈和本地方法栈溢出

无论是本地方法,还是Java方法,在执行时肯定依赖于函数栈帧这样的结构,一个方法调用的时候则会在虚拟机栈中创建一个栈桢,方法调用到结束的过程即为栈桢的入栈和出栈的过程。Hotspot虚拟机并不区分虚拟机栈和本地方法栈,Hotspot虚拟机的栈内存是无法自动扩容的。这样的话,可能会有两种情况造成栈内存溢出:

1、方法的调用太深,会导致栈桢创建的太多,耗尽了当前线程分配的栈的内存。例如死循环。为了方便测试,在运行时加上一些参数, -Xss256k 该参数表示栈内存最大为256k且不可扩展。

1public class StackOOM {
2    public static void main(String[] args) {
3        recursion();
4    }
5
6    public static void recursion(){
7        recursion();
8    }
9}

2、栈桢的大小过大,导致了耗尽了内存。那什么情况会导致栈桢的大小大呢?栈是由局部变量表、操作数栈、动态链接组成。如果方法的局部变量很多的话,调用时创建的栈桢占用的空间比较大,就有可能造成栈溢出。

方法区和运行时常量池溢出

首先要知道方法区中存放的是什么:存放已被虚拟机加载的类的信息,常量、静态变量。

下面是 JDK1.6 和 JDK1.8 的区别:

JDK1.7 之前运行时常量池逻辑包含字符串常量池存放在方法区,此时 Hotspot 虚拟机对方法区的实现为永久代,JDK1.7 字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区,也就是 Hotspot 中的永久代。

JDK1.8 Hotspot 移除了永久代用元空间(Metaspace)取而代之,这时候字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace)。 直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,比如NIO的 DirectByteBuffer 可以直接分配堆外内存,所以也可能导致 OutOfMemoryError 错误出现。

方法区溢出可以通过CGlib动态代理是通过生成代理类,然后加载到 JVM 中,所以可以通过不断的生成代理类来模拟方法区溢出:

 1// -XX:MetaspaceSize=21m -XX:MaxMetaspaceSize=21m
 2public class JavaMethodAreaOOM {
 3    public static void main(String[] args) {
 4        while (true) {
 5            Enhancer enhancer = new Enhancer();
 6            enhancer.setSuperclass(OOMObject.class);
 7            enhancer.setUseCache(false);
 8            enhancer.setCallback(new MethodInterceptor() {
 9                @Override
10                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
11                    return proxy.invokeSuper(obj, args);
12                }
13            });
14            enhancer.create();
15        }
16    }
17
18    static class OOMObject {
19    }
20}

本机直接内存溢出

直接内存(Direct Memory)的容量大小可通过 -XX:MaxDirectMemorySize参数来指定,默认与Java堆最大值(由-Xmx指定)一致。下面的代码就属于通过 ByteBuffer.allocateDirect 直接申请本机内存,但是出现了OOM:

 1import java.nio.ByteBuffer;
 2import java.util.ArrayList;
 3
 4public class ExtMemOOM {
 5    public static void main(String[] args) {
 6        ArrayList<ByteBuffer> list = new ArrayList<>();
 7        while (true){
 8            list.add(ByteBuffer.allocateDirect(1024 * 1024 * 1024));
 9        }
10    }
11}

Android中捕获OOM

在Android中的View.java 的 buildDrawingCacheImpl() 方法中有这么一段代码,但是在创建BitMap的时候, 如果没有足够的内存来创建BitMap缓存,则直接忽略这个问题, 因为在 View 的绘制过程中 Bitmap Cache 并不是必须存在的。所以在这里没有必要抛出 OOM ,而是自己捕获就可以了 :

 1private void buildDrawingCacheImpl(boolean autoScale) {
 2    // ......
 3    if (bitmap != null) bitmap.recycle();
 4
 5    try {
 6        bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(),
 7                                     width, height, quality);
 8        bitmap.setDensity(getResources().getDisplayMetrics().densityDpi);
 9        if (autoScale) {
10            mDrawingCache = bitmap;
11        } else {
12            mUnscaledDrawingCache = bitmap;
13        }
14        if (opaque && use32BitCache) bitmap.setHasAlpha(false);
15    } catch (OutOfMemoryError e) {
16        // If there is not enough memory to create the bitmap cache, just
17        // ignore the issue as bitmap caches are not required to draw the
18        // view hierarchy
19        if (autoScale) {
20            mDrawingCache = null;
21        } else {
22            mUnscaledDrawingCache = null;
23        }
24        mCachingFailed = true;
25        return;
26    }
27	// ......
28}

这就是一个try-catch OOM的一个典型用例。

下面是我在Android中测试try-catch OOM的例子,点击按钮就开启不停的创建对象:

 1public class MainActivity extends AppCompatActivity {
 2    private static final String TAG = "MainActivity";
 3    private ArrayList<OOMClass> list;
 4
 5    @Override
 6    protected void onCreate(Bundle savedInstanceState) {
 7        super.onCreate(savedInstanceState);
 8        setContentView(R.layout.activity_main);
 9        list = new ArrayList<>();
10    }
11	// bind button
12    public void mallocMem(View view) {
13        try {
14            while (true){
15                list.add(new OOMClass());
16            }
17        }catch (OutOfMemoryError error){
18            Log.i(TAG, "mallocMem: OutOfMemoryError!");
19        }
20    }
21
22    static class OOMClass { }
23}

如果把捕获 OOM 当做处理 OOM 的一种手段,无疑是不合适的。无法保证你 catch 的代码就是导致 OOM 的原因,可能它只是压死骆驼的最后一根稻草,甚至你也无法保证你的 catch 代码块中不会再次触发 OOM 。

正如在构建 Bitmap 对象的时候,如果捕捉到了 OOM ,就放弃生成 Bitmap 缓存,因为在 View 的绘制过程中 Bitmap Cache 并不是必须存在的。所以在这里没有必要抛出 OOM ,而是自己捕获就可以了。

所以在你自己明确知道可能发生 OOM 的情况下设置一个兜底策略,这可能是捕获 OOM 的唯一意义了。