Новый перевод от команды Spring АйО расскажет вам (с примерами кода), как JVM обрабатывает исключения на низком уровне, что такое таблица исключений и какие сценарии используются для вариантов try
-catch
и try
-finally
.
То, как JVM обрабатывает инструкции на байткоде, уже само по себе довольно интересно. Но знаете ли вы, что происходит, когда выбрасывается исключение? Как JVM справляется с передачей управления? Как это выглядит в байткоде?
Сейчас я вам по-быстренькому это покажу!
Пример
Вот небольшой кусочек кода на Java, в котором присутствуют все важные действующие лица, участвующие в игре в исключения (try
-catch
-finally
):
int a = 0;
try {
if (a == 0) { // to make it less boring, let's have some branching logic
throw new Exception("Exception message!");
}
} catch (Exception e) {
doSomethingOnException();
} finally {
doSomethingFinally();
}
Таблица исключений
Получившийся байткод включает в себя интересный фрагмент внутри атрибута Code, который называется таблицей исключений. Каждый метод может иметь свою собственную таблицу исключений, но она включается в код только тогда, когда ее присутствие имеет смысл. Если в методе отсутствует логика обработки исключений, то и таблицы исключений тоже не будет.
В нашем примере таблица исключений выглядит вот так:
Exception table:
from to target type
2 16 22 Class java/lang/Exception
2 16 32 any
22 26 32 any
Числа указывают на адреса байткод-инструкций. Каждая строка в таблице показывает диапазон байткод-инструкций (from
и to
), за которым следит обработчик исключений. Сам обработчик — это тоже всего лишь набор байткод-инструкций, а колонка target
указывает на адрес, где начинается код обработки. type
просто задает тип исключения, который обрабатывается тем или иным обработчиком.
Байткод-инструкции
Чтобы точно понять, куда ведут эти адреса, давайте посмотрим также на набор байткод-инструкций для данного метода (объяснение приведено ниже):
0: iconst_0
1: istore_0
2: iload_0
3: ifne 16
6: new #12 // class java/lang/Exception
9: dup
10: ldc #14 // String Exception message!
12: invokespecial #16 // Method java/lang/Exception."<init>":(Ljava/lang/String;)V
15: athrow
16: invokestatic #19 // Method doSomethingFinally:()V
19: goto 38
22: astore_1
23: invokestatic #22 // Method doSomethingOnException:()V
26: invokestatic #19 // Method doSomethingFinally:()V
29: goto 38
32: astore_2
33: invokestatic #19 // Method doSomethingFinally:()V
36: aload_2
37: athrow
38: return
Давайте пройдемся по этому коду.
Инструкции 0
- 2
отвечают за создание переменной int a = 0;
.
Дальше идет инструкция ifne
, которая представляет собой условный переход, а 16
— это адрес, на который надо перейти. Если a
не равна 0
, мы переходим на адрес 16
. Там мы находим содержимое нашего блока finally
, за которым идет инструкция goto
, ведущая в конец метода.
Если условие принимает значение false
, мы переходим к следующим инструкциям. Инструкции 6
- 12
создают экземпляр класса Exception
. Заметьте, что ссылка на этот экземпляр дублируется, поэтому к концу этого набора инструкций она появится в стеке операндов дважды.
Инструкция с адресом 15
, athrow
, выбрасывает само исключение! Она “съедает” одну из ссылок на экземпляр Exception
из стека операндов и таким образом узнает, какое именно исключение следует выбросить. По факту, она подготавливает стековый фрейм для обработки исключения (очищая его), оставляя только одну ссылку на исключение сверху.
Когда JVM встречает инструкцию athrow
, она проверяет таблицу исключений метода, чтобы найти подходящее местоположение, с которого следует для продолжить выполнение.
Сценарий try-catch-finally
Давайте посмотрим на таблицу исключений еще раз, ведь теперь у нас есть больше контекста.
Exception table:
from to target type
2 16 22 Class java/lang/Exception
2 16 32 any
22 26 32 any
Мы обнаружили athrow
по адресу 15
. Этот адрес покрывается первыми двумя дорожками таблицы, поскольку попадает в диапазон [2, 16)
.
Зачем нужны два обработчика? Один — это блок catch
(target
показывает на 22
, откуда начинается логика catch
), а второй — это блок finally
(target 32
указывает на логику finally
). Третья дорожка в таблице закрывает диапазон инструкций, содержащих нашу логику catch
, а target
указывает на блок finally
. Это означает, что если во время выполнения блока catch
что-то случится, мы все еще хотим оказаться в блоке finally
.
Когда мы находимся в блоке catch
(адрес 22
), какие-то данные из стека операндов сохраняются в переменной. Помните, у нас была дополнительная ссылка на экземпляр Exception
в нашем стеке? Здесь инструкция astore
сохраняет ее как переменную. Внутри JVM переменная не имеет имени, но по логике вещей это наша переменная e
, которую мы можем использовать в блоке catch
для логирования или других действий.
Менее удачный сценарий
Описанный выше сценарий — самый удачный. JVM поискала по таблице исключений и нашла обработчик, который знал, что делать с Exception
. Если бы существовал только обработчик finally
, этого было бы недостаточно.
Как JVM узнает, относится ли найденный обработчик к catch
или к finally
? Мы видим, что колонка type
для обработчика finally
содержит тип any
, что означает, что он должен быть выполнен в любом случае, независимо от того, было выброшено исключение или нет. Если вместо этого значения задать реальный тип, это будет обработчик для catch
. Тип, который ищет JVM, является точным совпадением с исключением или его супертипом.
Но что если подходящего обработчика нет? JVM завершает текущий метод и передаёт исключение вызывающему методу. Если в стеке вызовов нет подходящего обработчика, поток завершает работу с трассировкой стека.
Обобщенный сценарий
Механизм try
-catch
-finally
можно обобщить следующим образом:
Инструкция
athrow
говорит JVM, что выброшено исключение. Какое исключение? То, что находится сверху в стеке операндов.Когда это происходит, JVM проверяет таблицу исключений для текущего метода. Получив адрес инструкции
athrow
, она может найти адреса подходящих обработчиков (или одного обработчика).Если обнаружен обработчик
catch
, (обработчик, который имеет правильный тип исключения в колонкеtype
), ссылка на исключение сохраняется из стека в переменную. Затем выполняется логикаcatch
.Если найден обработчик
finally
, он выполняется.Если не найден подходящий обработчик
catch
, JVM прерывает выполнение текущего фрейма и ищет обработчик в предыдущем фрейме.JVM проходит через все фреймы, пока не найдет обработчик
catch
, выполняя все обработчикиfinally
, обнаруженные по пути.Если не найден ни один обработчик, выполнение программы прекращается.
![](https://habrastorage.org/getpro/habr/upload_files/a40/da0/b40/a40da0b40cd0298af40ddda054e2b06d.png)
![](https://habrastorage.org/r/w1560/getpro/habr/upload_files/f82/06b/9bd/f8206b9bd27786a8b1c004f0b9c3b147.png)
Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм - Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано