NonFatal能抓住Throwable吗?

mis*_*tor 8 java jvm scala exception-handling akka

据我所知,Java/JVM中的最佳实践要求你永远不要Throwable直接捕获,因为它涵盖的Error内容恰好包含OutOfMemoryErrorKernelError.这里这里有一些参考.

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

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

拆卸输出

问题:

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

Hol*_*ger 9

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

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

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

没有

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

但是,当然,有合理的用例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));
}
Run Code Online (Sandbox Code Playgroud) 结果:
holding lock: true
java.lang.OutOfMemoryError has been thrown, holding lock: false
Run Code Online (Sandbox Code Playgroud)

试穿与资源语句采用这种更进一步; 它可以通过记录close()操作抛出的后续抑制异常来修改挂起的throwable :

try(AutoCloseable c = () -> { throw new Exception("and closing failed too"); }) {
    throw new OutOfMemoryError();
}
Run Code Online (Sandbox Code Playgroud) 结果:
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
Run Code Online (Sandbox Code Playgroud)

此外,当您submit执行任务时ExecutorService,所有throwable将被捕获并记录在返回的未来:

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(); }
Run Code Online (Sandbox Code Playgroud) 结果:
caught and wrapped: java.lang.OutOfMemoryError
Run Code Online (Sandbox Code Playgroud)

在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());
}
Run Code Online (Sandbox Code Playgroud) 结果:
caught and wrapped: java.lang.OutOfMemoryError
Run Code Online (Sandbox Code Playgroud)

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();
Run Code Online (Sandbox Code Playgroud) 结果:
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
Run Code Online (Sandbox Code Playgroud)

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


Ale*_*lcu 7

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

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

至于它为什么这样做:

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

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

  • @Jasper-M不,我不同意; 例如,许多异步抽象尝试模仿JVM的调用堆栈并尝试/ catch/finally进行资源处理 - 例如,"finally"语句将始终执行,无论抛出什么"Throwable"以及是否构建任何需要的抽象基本上做什么`终于'做什么,然后通过不捕捉`ControlThrowable`这是一场等待发生的灾难.因为即使抛出的`ControlThrowable`代表一个错误 - (1)崩溃不一定便宜,不在JVM上,(2)其他库会捕获它(例如Netty). (2认同)