【问题标题】:Is it okay that NonFatal catches Throwable?NonFatal 可以捕获 Throwable 吗?
【发布时间】:2018-04-11 11:57:06
【问题描述】:

据我了解,Java/JVM 中的最佳实践要求您永远不要直接捕获Throwable,因为它涵盖了Error,而恰好包含OutOfMemoryErrorKernelError 之类的东西。一些参考herehere

然而,在 Scala 标准库中,有一个提取器 NonFatal 被广泛推荐(并被 Akka 等流行库广泛使用)作为您的 catch 块中的最终处理程序(如果需要)。正如怀疑的那样,这个提取器碰巧捕获了Throwable,如果它是致命错误之一,则重新抛出它。见代码here

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

问题:

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

【问题讨论】:

  • 应用程序不应该处理致命错误,因为它处于 JVM 级别
  • 如果您认为捕获Throwable 是不好的,因为它包含OutOfMemoryError 和其他致命错误,并且您知道NonFatal 过滤掉了这些致命错误,那么您为什么认为NonFatal 是还是不好?
  • 我认为这取决于您所说的“捕获”是什么意思。通常它意味着处理/吞下异常的catch 块。
  • @missingfaktor 我认为你是对的。它看起来不像是在捕捉,但它实际上确实捕捉到它并在字节码级别做一些事情,这就是过滤。
  • 请注意,当您使用try { … } finally {…}try( … ) { … } 时,捕捉Throwable 并在重新抛出它之前做某事总是会发生的。同样,当您使用 synchronized(…) { … } 时,它总是会发生

标签: java scala exception-handling jvm akka


【解决方案1】:

请注意,捕获Throwable 的频率比您可能意识到的要频繁。其中一些案例与 Java 语言功能紧密结合,这些功能可能会产生与您所展示的非常相似的字节码。

首先,由于finally 在字节码级别上没有挂起,它通过为Throwable 安装异常处理程序来实现,该处理程序将在重新抛出Throwable 之前执行finally 块的代码,如果代码流达到了这一点。此时你可能会做一些非常糟糕的事情:

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时,所有的throwables都会被捕获并记录在返回的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,这是内部使用的默认 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 是否会被捕获的技术细节,而是代码的语义。这是用于忽略异常(坏)还是用于尽管报告了严重的环境错误(坏)还是仅用于执行清理(好)?上面描述的大多数工具都可以用于好的和坏的......

【讨论】:

  • 赞成由代码示例支持的深入研究。
  • @AlexandruNedelcu 缺乏任何权威参考表明可以捕获并重新抛出 Throwable 只需在两个操作之间执行一点代码,这个答案至少提供了一些证据表明它被认为是可以这样做。您的回答说也可以,但是您没有提供证据。他在这里展示的证据来自 JVM 创建者,所以我不得不假设他们知道自己做了什么。
  • @AlexandruNedelcu 你读过答案的最后一段吗?它准确地回答了这个问题。捕捉Throwable 是否可以,取决于为什么你这样做。
  • @AlexandruNedelcu finallyfinalize() 无关。 finally 中的操作应该很简单,并且不依赖于额外的资源,例如比如释放锁或关闭资源。因为,正如所解释的,它只是 catch 块的变体,不需要额外的资源来执行它们。正如你所说的“捕捉和重新抛出 Throwable 不是问题”,但我会将其限制为语言功能和精心设计的框架,例如示例。但重要的是要理解,忽略 Futures 记录的异常就像捕捉 Throwable...
  • @AlexandruNedelcu 在 Java 的上下文中,将其称为“终结器”是一种误导。然而,我明白你的意思,但我认为,你很难决定哪个 throwable 允许尝试关闭套接字,哪个不允许。在期货的上下文中,不捕获错误只会杀死特定线程而根本不会启动关闭。事实上,如果未来没有捕获和报告,池所有者没有任何机会检测到它应该关闭......
【解决方案2】:

不建议捕获 throwable,因为您正在执行的任何处理都可能会延迟进程正确地崩溃(在内存不足错误的情况下),然后最终进入僵尸状态,垃圾收集器拼命尝试释放内存并冻结一切。因此,在某些情况下,您需要放弃您可能拥有的任何活跃交易并尽快崩溃。

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

至于为什么这样做:

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

也就是说 Scala 的 NonFatal 并不完美。例如,它还重新抛出了ControlThrowable,这是一个巨大的错误(以及 Scala 的非本地返回)。

【讨论】:

  • 如果你接受 Scala 中存在非本地返回,即使它们很糟糕,你是否同意它们永远不应该被用户代码捕获?
  • @Jasper-M 不,我不同意;例如许多异步抽象试图模仿 JVM 的调用堆栈和 try/catch/finally 以进行资源处理——例如,“finally”语句将始终被执行,无论抛出什么 Throwable 并且如果您正在构建任何需要的抽象基本上做finally 所做的事情,然后不赶上ControlThrowable,那就是一场等待发生的灾难。因为即使抛出的ControlThrowable 代表了一个错误——(1)崩溃不一定便宜,在 JVM 上也没有,(2)其他库会捕获它(例如 Netty)。
【解决方案3】:

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

从这个角度来看,捕获OutOfMemoryError 是没有任何意义的,因为如果发生这种情况,您将无法再信任您的 JVM,也无法在 @ 中修复您的程序状态987654323@块。

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

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

1) NonFatal 捕获 StackOverflowError(从我的角度来看这毫无意义)

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

【讨论】:

  • 自 2.11 起 NonFatal 重新抛出 StackOverflowError
  • @Jasper-M 你是对的。但是由于第二点,它仍然无法正确处理
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-11-04
  • 2013-12-12
  • 1970-01-01
  • 2012-02-12
  • 2017-11-05
  • 2013-07-24
  • 1970-01-01
相关资源
最近更新 更多