OutOfMemoryError 可以被 try-catch 吗

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

Java Heap 发生 OOM

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

1
-Xmx30m

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.ArrayList;

public class HeapOOM {
public static void main(String [] args) {
new HeapOOM().testOOM ();
}

public void testOOM() {
ArrayList<OOMClass> list = new ArrayList<>();
while (true){
list.add (new OOMClass());
}
}

static class OOMClass { }
}

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 且不可扩展。

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

public static void recursion(){
recursion ();
}
}

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

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

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

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

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

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

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

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

static class OOMObject {
}
}

本机直接内存溢出

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

1
2
3
4
5
6
7
8
9
10
11
import java.nio.ByteBuffer;
import java.util.ArrayList;

public class ExtMemOOM {
public static void main(String [] args) {
ArrayList<ByteBuffer> list = new ArrayList<>();
while (true){
list.add (ByteBuffer.allocateDirect (1024 * 1024 * 1024));
}
}
}

Android 中捕获 OOM

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void buildDrawingCacheImpl(boolean autoScale) {
//......
if (bitmap != null) bitmap.recycle ();

try {
bitmap = Bitmap.createBitmap (mResources.getDisplayMetrics (),
width, height, quality);
bitmap.setDensity (getResources ().getDisplayMetrics ().densityDpi);
if (autoScale) {
mDrawingCache = bitmap;
} else {
mUnscaledDrawingCache = bitmap;
}
if (opaque && use32BitCache) bitmap.setHasAlpha (false);
} catch (OutOfMemoryError e) {
// If there is not enough memory to create the bitmap cache, just
//ignore the issue as bitmap caches are not required to draw the
//view hierarchy
if (autoScale) {
mDrawingCache = null;
} else {
mUnscaledDrawingCache = null;
}
mCachingFailed = true;
return;
}
//......
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private ArrayList<OOMClass> list;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate (savedInstanceState);
setContentView (R.layout.activity_main);
list = new ArrayList<>();
}
//bind button
public void mallocMem(View view) {
try {
while (true){
list.add (new OOMClass());
}
}catch (OutOfMemoryError error){
Log.i (TAG, "mallocMem: OutOfMemoryError!");
}
}

static class OOMClass { }
}

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

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

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