NonFatal 捕获 Throwable 可以吗?

Is it okay that NonFatal catches Throwable?

据我了解,Java/JVM 中的最佳实践规定您永远不要直接捕获 Throwable,因为它涵盖了 Error,而 Error 恰好包含 OutOfMemoryErrorKernelError。一些参考文献 here and here

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

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

问题:

  1. 我在第一段中所做的假设是否正确?还是我假设抓不到 Throwable 是不正确的?
  2. 如果这个假设是正确的,NonFatal 的行为会导致严重的问题吗?如果没有,为什么不呢?

不推荐捕获 throwable,因为无论你在做什么处理都可能延迟进程正常崩溃(在内存不足错误的情况下),然后最终进入僵尸状态,垃圾收集器拼命试图释放内存并冻结一切。因此,在某些情况下,您需要放弃您可能拥有的任何活动事务并尽快崩溃。

然而,如果您正在做的是一个简单的过滤器,那么捕捉和 re-throwing Throwable 本身并不是问题。 NonFatal 正在评估 Throwable 是否是虚拟机错误,或者线程被中断等,或者换句话说,它正在寻找需要注意的实际错误。

至于为什么这样做:

  • 人们一直在辱骂 Throwable / Error
  • NonFatal 也在寻找类似 InterruptedException 的东西,这是人们不尊重的另一个最佳实践

也就是说 Scala 的 NonFatal 并不完美。例如它也是 re-throwing ControlThrowable,这是一个巨大的错误(连同 Scala 的 non-local returns)。

如果你捕捉到一个异常而没有进一步重新抛出它,这意味着你可以保证程序在 catch 块完成后保持正确的状态。

从这个角度来看,捕捉一个 OutOfMemoryError 没有任何意义,因为如果它发生了,你就不能再相信你的 JVM,也不能完全修复您的程序在 catch 块中。

在Java中建议最多捕获Exception,而不是ThrowableNonFatal 构造的作者对哪些异常是可修复的,哪些不是可修复的有一些不同的看法。

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

1) NonFatal 捕获 WhosebugError(从我的角度来看这没有意义)

2) case NonFatal(ex) =>是一个scala代码,在异常发生后必须由JVM执行。而此时JVM可能已经坏掉了。 我曾经在日志中遇到过类似 java.lang.NoClassDefFoundError for NonFatal 的问题,但真正的原因是 WhosebugError

请注意,捕获 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,它将在 re-throwing:

之前释放对象监视器
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[=16=](Throwables.java:63)
        at Throwables.example4(Throwables.java:65)
        ... 1 more

此外,当您 submit 一个任务到 ExecutorService 时,所有可抛出的对象都将被捕获并记录在返回的 future 中:

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 which is the default 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(Throwables.java:91)
    at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java:1736)
    ... 4 more

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