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