OutOfMemoryError 可以被 try catch 吗?
群里小伙伴碰到的一道比较经典的面试题,但我相信很多第一次碰到这个问题的同学应该无法立刻给出答案,最好的办法肯定还是动手测一测。
注意看下面的 Gif,每点击一次 Allocate 20MB ,都会给数组容量增加 20*1024*1024
,当然应该并不是 20 MB。如下面代码所示:
binding.allocate.setOnClickListener { try { bytes = ByteArray(bytes.size + 1024 * 1024 * 20) refreshMemory() } catch (e: OutOfMemoryError) { binding.oomError.text = "Catch OOM : \n ${e.message}" } } 复制代码
当点击第 7 次时,发生了 OutOfMemoryError
,并且 catch
代码块执行了。
Catch OOM : Failed to allocate a 146801680 byte allocation with 25165824 free bytes and 133MB until OOM, target footprint 153948888, growth limit 268435456
所以,OutOfMemoryError 是可以 try catch 的。
顺道画了一个思维导图回顾一下 Java 的异常体系。
上面的图片没有罗列出所有的异常类型,但也基本概括了 Java 异常的继承体系。所有的异常类都继承自 Throwable
,Throwable
有两个直接子类 Error
和 Exception
。
Exception
一般指可以/应该捕获和处理的异常。它的两个直接子类 IOException
和 RuntimeException
及其子类都是我们在代码中经常遇到的一些错误。
RuntimeException
是在程序运行中可能发生的异常,我们可以不捕获它,但可能带来 Crash 的代价,但是过多的捕获异常又不利于暴露和调试异常情况。在开发过程中,我们更多的应该及时暴露问题。除了 RuntimeException 以外,其他异常可以统称为 非运行时异常 或者 受检异常,这些异常必须被捕获,否则编译期就会报错。
Error
一般指非正常状态的,比较严重的,不应该被捕获的系统错误。
再回头看看 OutOfMemoryError 的父类们,
OutOfMemoryError <- VirtualMachineError <- Error
OutOfMemoryError 是一个 Error ,Error 不应该被捕获。那么,捕获 OutOfMemoryError 有什么意义呢?
捕获 OutOfMemoryError 有什么意义?
一般情况下并没有什么太大意义,相信你在开发中也几乎没有写过 catch OOM 的代码。
如果你把捕获 OOM 当做处理 OOM 的一种手段,无疑是不合适的。你无法保证你 catch 的代码就是导致 OOM 的原因,可能它只是压死骆驼的最后一根稻草,甚至你也无法保证你的 catch 代码块中不会再次触发 OOM 。
我也从来没有写过捕获 OOM 的代码,但无意中在 Android 源码中发现了这样的操作。在 View.java
的 buildDrawingCacheImpl()
方法中有这么一段代码:
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; ...... 复制代码
buildDrawingCacheImpl()
方法的大致作用是为当前 View 生成一个 Bitmap 缓存。在构建 Bitmap 对象的时候,如果捕捉到了 OOM ,就放弃生成 Bitmap 缓存,因为在 View 的绘制过程中 Bitmap Cache 并不是必须存在的。所以在这里没有必要抛出 OOM ,而是自己捕获就可以了。
在你自己明确知道可能发生 OOM 的情况下设置一个兜底策略,这可能是捕获 OOM 的唯一意义了。如果你有其他奇淫技巧,欢迎在评论区补充。
JVM 中哪一块内存不会发生 OOM ?
最后补充一道我曾经遇到过的面试题,JVM 中哪一块内存不会发生 OOM ?
当时面试的时候一下没反应过来,回来之后翻了翻 《深入理解Java虚拟机》 。但凡是 JVM 的相关问题,基本上都可以在这本书上找到答案。以下内容均总结摘抄自这本书,也可以查看我的相关读书笔记:第2章:Java内存区域与内存移溢出异常 。
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,如下图所示:
Java 虚拟机栈 。每个方法被执行的时候,Java 虚拟机栈都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每个方法被调用直到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。 如果 Java 虚拟机栈支持动态扩展,当栈扩展时无法申请到足够的内存会排抛出 OutOfMemoryError 异常。
本地方法栈。为虚拟机使用到的 Native 方法服务。《Java 虚拟机规范》对本地方法栈中方法使用的语言、使用方式和数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它。Hotspot 将本地方法栈和虚拟机栈合二为一。
本地方法栈也会在栈深度溢出和栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 。
Java 堆。所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里 “几乎” 所有的对象实例都在这里分配内存。在 《Java 虚拟机规范》中对 Java 堆的描述是:“所有的对象实例以及数组都应当在堆上分配”。
Java 堆以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。
Java 堆既可以被实现成固定大小,也可以是扩展的。如果在 Java 堆中没有内存完成实例分配,并且堆无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 。
方法区。方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
虽然《Java 虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做“非堆”,目的是与 Java 堆分开来。
Hotspot 设计之初选择把垃圾收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,使得 HotSpot 的 GC 能够像管理 Java 堆一样管理这部分内存,但导致 Java 应用更容易遇到内存溢出的问题。在 JDK 8 中,彻底废弃了永久代的概念。
如果方法区无法满足新的内存分配的需求时,将抛出 OutOfMemoryError 。
运行时常量池。方法区的一部分。Class 文件的常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后方法方法去的运行时常量池。
运行时常量池具有动态性,运行期间也可以将新的常量放入池中,如 String.intern() 。
常量池受到方法区的限制,当无法再申请到内存时,会抛出 OutOfMemoryError 。
唯一一个在《Java虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域是 程序计数器。程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。
最后
这是专栏第五篇文章了,写作素材大多来自于身边的小伙伴。我也维护了一份 面试题文档,但考虑到共享文档比较容易造成混乱,后面也可能通过其他方式进行分享。