为何Java中OutOfMemoryError的危险性高于其他异常?
OutOfMemoryError真的比其他异常更危险吗? 技术社区长期以来的共识是:除了必要的资源清理操作,不要捕获java.lang.OutOfMemoryError,出现OOM直接让JVM崩溃重启就行,核心理由是OOM可能导致进程进入无效状态。这个说法乍看合理,但仔细想就会有疑问:任何异常没处理好都会留无效状态,为什么捕获Exception是规范,改成捕获Throwable就成了反面教材?
比如下面这段常见的请求处理代码:
String handleRequest(String name) { try { if (name.equals("buggy")) return buggy(); else if (name.equals("hungry")) return hungry(); else throw new IllegalArgumentException(); } catch (Exception e) { // 只捕获Exception,不捕获OOM等Error return "Error: " + e; } } String buggy() { bunnyLock.lock(); // 这里用了round而非floor,可能抛出数组越界异常 var res = bunnies[(int) Math.round(Math.random() * bunnies.length)]; bunnyLock.unlock(); return res; } String hungry() { var b = new byte[2_000_000_000]; // 尝试分配2GB内存 ... }
这段代码里,会抛ArrayIndexOutOfBoundsException的buggy()方法明显会遗留锁未释放的脏状态,反而可能抛OOM的hungry()方法,在分配大数组失败时不会修改任何共享状态,看起来更安全。
你可能会说,只要把buggy()改成正确的try-finally写法,保证锁一定释放,就能解决状态问题:
String buggy() { bunnyLock.lock(); try { return bunnies[(int) Math.round(Math.random() * bunnies.length)]; } finally { bunnyLock.unlock(); } }
如果所有代码都能做到这个级别的异常安全,是不是捕获OOM也没问题?你没有遗漏什么关键逻辑,OOM本身并没有什么“会腐蚀进程状态”的魔法,大家不推荐捕获Throwable、不推荐随便catch OOM,本质是几个非常现实的工程考量,和“状态无效”的关系其实没那么大:
- OOM的抛出点完全不可预测
你举的hungry()方法里,OOM是在显式分配大数组时抛出的,这时候确实不会留脏状态。但JVM里OOM可能在任何一行代码发生:哪怕是你调用lock()之后、进入try块之前的那一行,可能刚好因为JIT编译、类加载、栈上临时变量分配触发OOM,这时候finally块还没注册,锁照样会泄漏。而普通的Exception比如数组越界、空指针,抛出点是完全可预期的,只要正确写try-finally就能100%覆盖所有异常路径。 - catch到OOM之后,你的错误处理逻辑大概率跑不通
普通Exception发生时,JVM的内存、线程资源都是正常的,你打日志、返回错误响应的逻辑可以正常执行。但OOM发生意味着堆内存已经满了,你catch住之后想拼接错误字符串、想写日志、想返回响应,每一步都需要分配新对象,很可能直接再抛一次OOM,最后你什么错误信息都捞不到,还把原始的OOM栈给吞了,排查问题根本无从下手。 Throwable包含了大量根本无法恢复的错误
不推荐catchThrowable不只是针对OOM:StackOverflowError发生时,线程栈已经耗尽,你根本不知道当前执行到了哪一步,后续代码的调用逻辑都不一定完整;LinkageError、NoClassDefFoundError这类类加载错误发生时,说明程序的字节码依赖已经出问题了,后续再调用相关类只会继续报错;还有线程中断相关的InterruptedException如果被乱吞,会导致线程池的停机逻辑完全失效。这些错误发生后,你没有任何办法确认程序的哪些组件还能正常工作,与其硬扛着返回一个可能错误的结果,不如直接崩溃重启,至少重启后的服务是100%可用的。- “全代码异常安全”是理想状态,工程上要考虑落地成本
要求所有代码都做到“任何异常/错误抛出都不遗留脏状态”当然是对的,但现实里99%的业务项目做不到。大部分开发写代码时只会考虑显式抛出的业务异常,不会去考虑每一行会不会抛OOM、会不会抛栈溢出,锁没释放、流没关、静态变量被改了一半的情况比比皆是。与其要求所有人都写完美的异常安全代码,不如定一个简单可执行的规则:不要在入口处统一吞Error,出现OOM直接重启,这个方案的落地成本极低,可靠性反而更高。
最后要明确:不是说绝对不能catch OOM。如果你在非常边缘的独立逻辑里,明确知道某个操作抛OOM不会影响核心共享状态,完全可以catch住做降级——比如图片服务尝试加载原图内存不够,catch住返回提前缓存好的缩略图,这是非常合理的优化。规范真正禁止的,是在最外层请求入口不分青红皂白catch Throwable,把所有Error都吞掉假装请求处理成功的行为。
内容的提问来源于stack exchange,提问作者Bart van Heukelom

