0%

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 的唯一意义了。

  • 本文作者: Tim
  • 本文链接: https://zouchanglin.cn/3390676345.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!