Это нормально, что nonfatal ловит throwable?

Насколько я понимаю, лучшая практика в Java / JVM диктует, что вы никогда не должны ловить Throwable напрямую, поскольку он охватывает Error, который, как правило, охватывает такие вещи, как OutOfMemoryError и KernelError. Некоторые ссылки здесь и здесь.

Однако в стандартной библиотеке Scala есть экстрактор NonFatal, который широко рекомендуется (и широко используется популярными библиотеками, такими как Akka) в качестве окончательного обработчика (если он вам нужен) в ваших блоках catch. Этот экстрактор, как и предполагалось, случайно перехватывает Throwable и повторно выбрасывает его, если это одна из фатальных ошибок. Смотрите код здесь.

Это может быть дополнительно подтверждено некоторым дизассемблированным байт-кодом:

Disassembled output

Вопросов:

  1. Верно ли предположение, сделанное мной в первом абзаце? Или я ошибаюсь, полагая, что ловить Throwable нехорошо?
  2. Если это предположение верно, может ли поведение NonFatal привести к серьезным проблемам? Если нет, то почему?

Приложение не предназначено для обработки фатальной ошибки, так как оно находится на уровне JVM.

cchantep 11.04.2018 14:07

Если вы думаете, что перехват Throwable - это плохо, потому что он включает в себя OutOfMemoryError и другие фатальные ошибки, и вы знаете, что NonFatal отфильтровывает эти фатальные ошибки, то почему вы думаете, что NonFatal все еще плох?

Jasper-M 11.04.2018 14:49

Я думаю, это зависит от того, что вы подразумеваете под «уловом». Обычно это означает блок catch, который обрабатывает / проглатывает исключение.

Ionuț G. Stan 11.04.2018 14:55

@ Jasper-M, перед повторным выбросом, он "ловит" его, и мой вопрос в том, создает ли это при этом проблемы.

missingfaktor 11.04.2018 15:25

@ IonuțG.Stan, понятно.

missingfaktor 11.04.2018 15:26

@missingfaktor Думаю, ты прав. Это не похоже на ловлю, но на самом деле ловит и что-то делает на уровне байт-кода, а именно фильтрацию.

Ionuț G. Stan 11.04.2018 15:28

Обратите внимание, что ловить Throwable и делать что-то перед его повторным выбросом всегда происходит при использовании try { … } finally {…} или try( … ) { … }. Точно так же всегда происходит при использовании synchronized(…) { … }.

Holger 11.04.2018 17:59
9
7
982
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Перехватывать throwable не рекомендуется, потому что любая выполняемая вами обработка может задержать законный сбой процесса (в случае ошибки нехватки памяти), а затем его переход в состояние, подобное зомби, когда сборщик мусора отчаянно пытается освободить память. и все замораживает. Таким образом, есть случаи, когда вам нужно отказаться от любых активных транзакций, которые у вас могут быть, и завершить работу как можно скорее.

Однако поймать и повторно выбросить Throwable само по себе не проблема, если то, что вы делаете, представляет собой простой фильтр. И NonFatal оценивает этот Throwable, чтобы увидеть, является ли это ошибкой виртуальной машины, прерыванием потока и т. Д., Или, другими словами, он ищет фактические ошибки, на которые следует обратить внимание.

Что касается того, почему он это делает:

  • люди злоупотребляли Throwable / Error
  • NonFatal также ищет такие вещи, как InterruptedException, что является еще одной передовой практикой, которую люди не уважают.

Тем не менее, NonFatal Scala не идеален. Например, он также повторно выбрасывает ControlThrowable, что является огромной ошибкой (наряду с нелокальными возвратами Scala).

Если вы согласны с тем, что в Scala существуют нелокальные возвраты, даже если они плохие, разве вы не согласны с тем, что они никогда не должны перехватывать пользовательский код?

Jasper-M 11.04.2018 14:50

@ Джаспер-М, нет, я не согласен; например многие асинхронные абстракции пытаются имитировать стек вызовов JVM и try / catch / finally для обработки ресурсов - например, оператор "finally" всегда будет выполняться, независимо от того, какой Throwable был брошен, и если вы создаете какую-либо абстракцию, которая должна делать в основном то, что делает finally, то, не поймав ControlThrowable, ждет катастрофа. Потому что, хотя брошенный ControlThrowable представляет собой ошибку - (1) сбои не обязательно дешевы, не на JVM и (2) другие библиотеки поймают его (например, Netty).

Alexandru Nedelcu 11.04.2018 15:02

Если вы поймаете исключение, не генерируя его повторно, это означает, что вы можете гарантировать, что программа останется в правильном состоянии после завершения блока catch.

С этой точки зрения нет никакого смысла ловить, скажем, OutOfMemoryError, потому что, если это произошло, вы больше не можете доверять своей JVM и не можете надежно восстановить состояние вашей программы в блоке catch.

В Java рекомендуется ловить не больше Exception, а не Throwable. У авторов конструкции NonFatal несколько иное мнение о том, какие исключения можно исправить, а какие нет.

В scala я предпочитаю ловить NonFatal вместо Exceptions, но ловушка исключений, как в Java, по-прежнему действует. Но будьте готовы к сюрпризам:

1) NonFatal ловит StackOverflowError (с моей точки зрения смысла нет)

2) case NonFatal(ex) => - это код Scala, который должен выполняться JVM после того, как исключение уже произошло. И JVM уже может быть взломана в этот момент. Однажды я столкнулся с чем-то вроде java.lang.NoClassDefFoundError для NonFatal в своих журналах, но настоящая причина была в StackOverflowError.

Начиная с 2.11 NonFatal заменяет StackOverflowError.

Jasper-M 11.04.2018 15:17

@ Джаспер-М, ты прав. Но он все еще не может правильно с этим справиться из-за второго пункта

simpadjo 11.04.2018 15:27
Ответ принят как подходящий

Обратите внимание, что обнаружение Throwable происходит чаще, чем вы думаете. Некоторые из этих случаев тесно связаны с функциями языка Java, которые могут создавать байтовый код, очень похожий на тот, который вы показали.

Во-первых, поскольку на уровне байт-кода нет привязки к finally, он реализуется путем установки обработчика исключений для Throwable, который будет выполнять код блока finally перед повторным запуском Throwable, если поток кода достигает этой точки. На этом этапе вы можете сделать действительно плохие вещи:

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

Ничего такого

try
{
    throw new OutOfMemoryError();
}
finally
{
    // highly discouraged too, throwing in finally shadows any throwable
    throw new RuntimeException("has something happened?");
}
Result:
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));
}
Result:
holding lock: true
java.lang.OutOfMemoryError has been thrown, holding lock: false

Оператор попробовать-с-ресурсом идет еще дальше; он может модифицировать ожидающий бросок, записывая последующие подавленные исключения, генерируемые операцией (операциями) close():

try(AutoCloseable c = () -> { throw new Exception("and closing failed too"); }) {
    throw new OutOfMemoryError();
}
Result:
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, все бросаемые объекты будут пойманы и записаны в возвращенном 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(); }
Result:
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());
}
Result:
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();
Result:
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, а на семантике кода. Используется ли это для игнорирования исключений (плохо) или для попытки продолжить, несмотря на сообщения о серьезных ошибках среды (плохо), или просто для выполнения очистки (хорошо)? Большинство описанных выше инструментов можно использовать как во благо, так и во вред ...

Проголосовали за тщательное исследование, подкрепленное примерами кода.

Ionuț G. Stan 12.04.2018 12:14

@ IonuțG.Stan не отвечает на вопрос - что действительно нормально для StackOverflow 🙂

Alexandru Nedelcu 12.04.2018 14:12

@AlexandruNedelcu не имеет какой-либо авторитетной ссылки на то, что можно поймать и повторно выбросить Throwable с небольшим количеством кода, выполняемым между двумя действиями, этот ответ предоставляет по крайней мере некоторые доказательства того, что это считается нормальным для этого. В вашем ответе сказано, что это тоже нормально, но вы не предоставляете доказательств. И доказательства, которые он здесь показывает, исходят от создателей JVM, поэтому я должен предположить, что они вроде как знали, что они сделали.

Ionuț G. Stan 12.04.2018 14:20

@AlexandruNedelcu, вы читали последний абзац ответа? Это точно отвечает на вопрос. Можно ли поймать Throwable, зависит от Зачем, которым вы это делаете.

Holger 12.04.2018 15:18

@Holger да, последний абзац не затрагивает этот вопрос, и на самом деле это большая проблема с отловом любой метательный для выполнения финализаторов, плюс тот факт, что Java выполняется «наконец» независимо от throwable, является большой ошибкой - b / c, когда процесс не хватает памяти, вероятно, он не сможет выполнить финализаторы - и я видел это на практике. Не поймите меня неправильно, ваш ответ классный и очень познавательный, спасибо за это, у меня просто очень старая домашняя мозоль с ТАК ответами, которые не отвечают на вопрос, многие авторы пытаются читать между строк 🙂

Alexandru Nedelcu 12.04.2018 16:09

@AlexandruNedelcu finally не имеет ничего общего с finalize(). Действия в finally должны быть простыми и не зависеть от дополнительных ресурсов, например например, снятие блокировки или закрытие ресурса. Поскольку, как объяснялось, это всего лишь вариант блока catch, для их выполнения не требуются дополнительные ресурсы. Как вы сами сказали, «поймать и повторно выбросить Throwable не проблема для каждого», но я бы ограничился языком функции и тщательно разработанные фреймворки, как в примерах. Но важно понимать, что игнорирование исключений, записанных Future, похоже на ловлю Throwable ...

Holger 12.04.2018 16:21

@Holger n.b. Я не говорю о finalize(), я даже не думал об этом, я говорю об общем понятии финализаторов. Также обратите внимание, что закрытие файлового сокета далеко не просто и недорого, потому что оно включает в себя системные вызовы, которые запускают, по крайней мере, переключение контекста, не говоря уже о таких операциях, как fsync или даже полное рукопожатие закрытия TCP.

Alexandru Nedelcu 12.04.2018 16:47

@AlexandruNedelcu в контексте Java неправильно называть его «финализатор». Тем не менее, я понимаю вашу точку зрения, но я думаю, вам будет сложно решить, какой бросаемый объект позволяет попытаться закрыть сокет, а какой нет. В контексте фьючерсов, если не будет обнаружена ошибка, будет просто уничтожен конкретный поток, не инициируя завершения работы вообще. Фактически, у владельца пула не было возможности обнаружить, что он должен отключиться, если будущее не уловит и не доложит ...

Holger 12.04.2018 17:25

Другие вопросы по теме