NonFatal能抓住Throwable吗?

时间:2018-04-11 11:57:07

标签: java scala exception-handling jvm akka

据我所知,Java / JVM中的最佳实践要求您永远不要直接捕获Throwable,因为它涵盖Error,其恰好包含OutOfMemoryError和{{KernelError 1}}。一些参考文献herehere

但是在Scala标准库中,有一个提取器NonFatal被广泛推荐(并被广泛使用的流行库如Akka广泛使用)作为catch中的最终处理程序(如果需要)块。怀疑这个提取器恰好捕获Throwable并重新抛出它,如果它是致命错误之一。请参阅代码here

这可以通过一些反汇编的字节码进一步证实:

Disassembled output

问题:

  1. 我在第一段中做出的假设是否正确?或者我错误地认为不可能抓住Throwable
  2. 如果这个假设是正确的,那么NonFatal的行为会导致严重的问题吗?如果没有,为什么不呢?

3 个答案:

答案 0 :(得分:9)

请注意,捕捉Throwable比您可能意识到的更频繁。其中一些案例与Java语言特性紧密结合,可能产生与您所显示的字节代码非常相似的字节代码。

首先,由于字节码级别上没有finally,因此通过安装Throwable的异常处理程序来实现它,该异常处理程序将在重新抛出之前执行finally块的代码如果代码流到达该点,则Throwable。你现在可以做坏事:

try
{
    throw new OutOfMemoryError();
}
finally
{
    // highly discouraged, return from finally discards any throwable
    return;
}
结果:

没有

try
{
    throw new OutOfMemoryError();
}
finally
{
    // highly discouraged too, throwing in finally shadows any throwable
    throw new RuntimeException("has something happened?");
}
结果:
java.lang.RuntimeException: has something happened?
    at Throwables.example2(Throwables.java:45)
    at Throwables.main(Throwables.java:14)

但是,当然,finally有合法的用例,就像进行资源清理一样。使用类似字节码模式的相关构造是synchronized,它将在重新抛出之前释放对象监视器:

Object lock = new Object();
try
{
    synchronized(lock) {
        System.out.println("holding lock: "+Thread.holdsLock(lock));
        throw new OutOfMemoryError();
    }
}
catch(Throwable t) // just for demonstration
{
    System.out.println(t+" has been thrown, holding lock: "+Thread.holdsLock(lock));
}
结果:
holding lock: true
java.lang.OutOfMemoryError has been thrown, holding lock: false

try-with-resource声明更进一步;它可以通过记录close()操作抛出的后续抑制异常来修改挂起的throwable:

try(AutoCloseable c = () -> { throw new Exception("and closing failed too"); }) {
    throw new OutOfMemoryError();
}
结果:
java.lang.OutOfMemoryError
    at Throwables.example4(Throwables.java:64)
    at Throwables.main(Throwables.java:18)
    Suppressed: java.lang.Exception: and closing failed too
        at Throwables.lambda$example4$0(Throwables.java:63)
        at Throwables.example4(Throwables.java:65)
        ... 1 more

此外,当您submit任务到ExecutorService时,所有投掷将被捕获并记录在返回的未来中:

ExecutorService es = Executors.newSingleThreadExecutor();
Future<Object> f = es.submit(() -> { throw new OutOfMemoryError(); });
try {
    f.get();
}
catch(ExecutionException ex) {
    System.out.println("caught and wrapped: "+ex.getCause());
}
finally { es.shutdown(); }
结果:
caught and wrapped: java.lang.OutOfMemoryError

如果JRE提供了执行程序服务,则责任在于FutureTask,这是内部使用的默认RunnableFuture。我们可以直接证明这种行为:

FutureTask<Object> f = new FutureTask<>(() -> { throw new OutOfMemoryError(); });
f.run(); // see, it has been caught
try {
    f.get();
}
catch(ExecutionException ex) {
    System.out.println("caught and wrapped: "+ex.getCause());
}
结果:
caught and wrapped: java.lang.OutOfMemoryError

但是CompletableFuture表现出类似捕捉所有掷骰子的行为。

// using Runnable::run as Executor means we're executing it directly in our thread
CompletableFuture<Void> cf = CompletableFuture.runAsync(
    () -> { throw new OutOfMemoryError(); }, Runnable::run);
System.out.println("if we reach this point, the throwable must have been caught");
cf.join();
结果:
if we reach this point, the throwable must have been caught
java.util.concurrent.CompletionException: java.lang.OutOfMemoryError
    at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:314)
    at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:319)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1739)
    at java.base/java.util.concurrent.CompletableFuture.asyncRunStage(CompletableFuture.java:1750)
    at java.base/java.util.concurrent.CompletableFuture.runAsync(CompletableFuture.java:1959)
    at Throwables.example7(Throwables.java:90)
    at Throwables.main(Throwables.java:24)
Caused by: java.lang.OutOfMemoryError
    at Throwables.lambda$example7$3(Throwables.java:91)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1736)
    ... 4 more

所以最重要的是,你不应该关注Throwable是否会被某个地方捕获的技术细节,而是代码的语义。这是用于忽略异常(坏)还是试图继续,尽管已经报告了严重的环境错误(坏)或只是为了执行清理(好)?上面描述的大多数工具都可以用于好的和坏的......

答案 1 :(得分:7)

不推荐捕获throwable,因为你正在进行的任何处理可能会使进程正常崩溃(在出现内存不足错误的情况下),然后以类似僵尸的状态结束,垃圾收集器拼命地尝试释放记忆,冻结一切。因此,在某些情况下,您需要放弃您可能拥有的任何活动交易并尽快崩溃。

然而,如果你正在做的是一个简单的过滤器,捕获和重新投掷Throwable本身并不是问题。并且NonFatal正在评估Throwable以查看它是虚拟机错误,还是线程被中断等,或者换句话说,它正在寻找需要注意的实际错误。

至于为什么这样做:

  • 人们一直在滥用Throwable / Error
  • NonFatal也在寻找像InterruptedException这样的东西,这是人们不尊重的另一种最佳做法

那说Scala的NonFatal并不完美。例如,它也在重新投掷ControlThrowable,这是一个巨大的错误(以及Scala的非本地回报)。

答案 2 :(得分:1)

如果您在不重新抛出异常的情况下捕获异常,则意味着您可以保证在catch块完成后程序保持正确状态。

从这个角度来看,抓住一个OutOfMemoryError没有任何意义,因为如果它已经发生了你不能再相信你JVM并且不能完全修复程序的状态在catch区块。

在Java中,建议最多抓住Exception,而不是ThrowableNonFatal构造的作者对哪些例外可以修复而哪些不可修复有不同的看法。

在scala中,我更喜欢捕获NonFatal而不是Exceptions,但是在Java中捕获异常仍然有效。 但要为惊喜做好准备:

1) NonFatal捕获StackOverflowError(从我的角度来看没有任何意义)

2)case NonFatal(ex) =>是一个scala代码,必须在异常发生后由JVM执行。而JVM现在已经被打破了。 我在日志中遇到java.lang.NoClassDefFoundError NonFatal之类的内容,但真正的原因是StackOverflowError