Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Компиляция Try/Catch/Finally для JVM

Anna | 2.06.2014 | нет комментариев

Взамен вступления

Автор статьи, Alan Keefer1, является основным зодчим компании Guidewire Software2, разрабатывающей программное обеспечение для страхового бизнеса. Еще будучи старшим разработчиком, он участвовал в работе над языком Gosu3. В частности, Алан занимался вопросами компиляции языка в байт-код Java.

Данная статья написана в 2009 году и посвящена деталям реализации try/catch/finally в JVM версии 1.6. Для ее прочтения нужно иметь базовые познания синтаксиса Java, а также понимать предназначение байт-кода, простыни которого лежат под катом. Также в конце статьи приведен ряд примеров, схожих на каверзные задачи SCJP.

Внутренности JVM

Одной из пророческой, над которой по целому ряду причин мы теперь трудимся, является компиляция нашего «домашнего» языка в байт-код Java. (Для справки: не могу сказать, когда мы завершим. Даже приблизительно. Даже попадет ли он в грядущие релизы.) Развлечение заключается в постижении внутренностей JVM, а также поиске всех долбанутых острых углов собственного языка. Но огромнее каждого «развлеченья» и острых углов доставляют такие операторы, как try/catch/finally. Следственно, на данный раз, я не буду вдаваться в философию либо аджайл. Взамен этого я углублюсь в JVM, куда большинству не требуется (либо не хочется) углубляться.

Если бы две недели назад вы спросили меня о finally-блоках, я бы предположил, что их обработка реализована в JVM: это базовая часть языка, она должна быть встроенной, не так ли? Каково же было мое изумление, когда я узнал: нет, не так. На самом деле finally-блоки легко подставляются во все допустимы места позже try- либо связанных с ним catch-блоков. Эти блоки оборачиваются в «catch(Throwable)», тот, что вторично выкинет исключение позже того, как finally-блок завершит работу. Осталось только подкрутить таблицу исключений, Дабы подставленные finally-блоки были пропущены. Ну как? (Маленький нюанс: до версии JVM 1.6 для оператора finally, по каждой видимости, применялись подпограммы взамен полной подстановки. Но теперь мы говорим о версии 1.6, к которой все вышесказанное применимо.)

Дабы осознать, имеет ли такой подход толк, отмотаем немножко назад и посмотрим, как JVM обрабатывает исключительные обстановки. Их обработка встроена в JVM в виде декларирования try/catch-блоков с поддержкой особого способа. Все, что от вас требуется, это сказать «между точкой A и точкой B всякое исключение типа E должно быть обработано кодом в точке C». Вы можете иметь столько таких деклараций, сколько понадобится. Когда исключение будет передано в данный способ, JVM обнаружит соответствующий catch-блок в зависимости от его типа.

Примитивный пример try/catch-блока

Разглядим примитивный пример:

public void simpleTryCatch() {
  try {
    callSomeMethod();
  } catch (RuntimeException e) {
    handleException(e);
  }
}

Для него, в финальном результате, вы получите байт-код, представленный ниже. (Я использую форматирование, которое предлагает ASM Eclipse — бесценный инструмент для постижения механизмов работы JVM. Мне кажется, что код в таком формате достаточно легко читать. «L0» и т.п. — это метки кода.)

public simpleTryCatch()V
TRYCATCHBLOCK L0 L1 L2 java/lang/RuntimeException
L0
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callSomeMethod()V
L1
  GOTO L3
L2
  ASTORE 1
  ALOAD 0
  ALOAD 1
  INVOKEVIRTUAL test/SimpleTryCatch.handleException(Ljava/lang/RuntimeException;)V
L3
  RETURN

Выходит, мы говорим оператору catch покрывать каждый try-блок целиком (но не оператор GOTO в конце), а в случае RuntimeException передать управление в L2. Если оператор try закончен, необходимо перепрыгнуть через оператор catch и продолжить выполнение. Если же вызван обработчик RuntimeException, исключение находится на вершине стека, и мы сберегаем его в локальную переменную. После этого мы загружаем указатель на «this» и исключение в таком порядке, Дабы вызвать способ handleException. Позже этого оставшийся код выполняется до конца. Впрочем если бы тут был добавочный catch-блок, мы бы его перепрыгнули.

Пример try/catch/finally-блока

Сейчас добавим finally-блок и добавочный оператор catch и посмотрим, что произойдет в байт-кодом. Возьмем дальнейший абсолютно выдуманный пример:

public void tryCatchFinally(boolean arg) {
  try {
    callSomeMethod();
    if (arg) {
      return;
    }
    callSomeMethod();
  } catch (RuntimeException e) {
    handleException(e);
  } catch (Exception e) {
    return;
  } finally {
    callFinallyMethod();
  }
}

В таком случае мы получим куда как менее внятный байт-код:

public tryCatchFinally(Z)V
TRYCATCHBLOCK L0 L1 L2 java/lang/RuntimeException
TRYCATCHBLOCK L3 L4 L2 java/lang/RuntimeException
TRYCATCHBLOCK L0 L1 L5 java/lang/Exception
TRYCATCHBLOCK L3 L4 L5 java/lang/Exception
TRYCATCHBLOCK L0 L1 L6
TRYCATCHBLOCK L3 L7 L6
TRYCATCHBLOCK L5 L8 L6
L0
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callSomeMethod()V
L9
  ILOAD 1
  IFEQ L3
L1
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callFinallyMethod()V
L10
  RETURN
L3
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callSomeMethod()V
L4
  GOTO L11
L2
  ASTORE 2
L12
  ALOAD 0
  ALOAD 2
  INVOKEVIRTUAL test/SimpleTryCatch.handleException(Ljava/lang/RuntimeException;)V
L7
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callFinallyMethod()V
  GOTO L13
L5
  ASTORE 2
L8
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callFinallyMethod()V
  RETURN
L6
  ASTORE 3
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callFinallyMethod()V
  ALOAD 3
  ATHROW
L11
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callFinallyMethod()V
L13
  RETURN

Выходит, что же тут происходит? (Обратите внимание, что метки пронумерованы в том порядке, в каком они сделаны компилятором, а не в порядке их возникновения в коде.) В первую очередь вы подметите, что оба блока обработки исключений сейчас поделены на два: с L0 до L1 и с L3 до L4. Это случилось по причине того, что между L1 и L3 из-за оператора return был вставлен finally-блок.

По той причине, что исключения, кинутые из finally-блока, не обязаны быть обработаны catch-блоками, связанными с тем же оператором try, соответствующий диапазон был убран из таблицы исключений. Записи в таблице без типа исключения как раз относятся к finally-блоку. Они обязаны обрабатывать исключения всякого типа, кинутые из оператора try либо из catch-блоков, при этом они обязаны игнорировать всякие подставленные finally-блоки. Таким образом finally-блоки не будут ловить исключения, кинутые такими же finally-блоками. Сходственных записей получилось три, потому что в дополнение к finally, подставленному вовнутрь try-блока, блок «catch(Exception)» так же содержит оператор return.

Так же вы можете поразиться увидев, что finally-блок встречается в коде 5 (пять) раз. 1-й подставленный finally, соответствующий оператору return try-блока, встречается между L1 и L3. 2-й finally-блок чуть больше запутан: он подставлен в конец первого catch-блока, тот, что после этого прыгает через остальной finally код. (Лично я предполагаю, что тут необходимо было сделать переход в конец взамен очередного встраивания.) 3-й раз он возникает между L8 и L6 перед оператором return во втором catch-блоке. Четвертый раз finally-блок возникает в коде между L6 и L11, что соответствует случаю появления исключительной обстановки: необходимо быть уверенными, что finally-блок выполнится в случае необработанного исключения, кинутого в try-блоке либо любом catch-блоке. Обратите внимание, что исключение как ни в чем не бывало сохраняется, осуществляется вызов оператора finally, позже чего исключение загружается и кидается вновь. В конечный finally-блок управление переходит из конца try-блока.

Если бы у нас были вложенные блоки try/catch либо try/finally, все было бы еще больше необычным. Оператор return внутреннего try-блока требует, Дабы перед ним были подставлены finally-блоки как внутреннего, так и внешнего try. Таблица исключений должна быть настроена таким образом, Дабы исключение, кинутое внутренним finally, было поймано внешними операторами catch и finally, а исключение, кинутое внешним finally, не было поймано ни кем. Теперь вы, вероятно, пытаетесь представить, какой комплект состояний ваш компилятор вынужден таскать с собой, Дабы знать, что куда подставить и как заполнить таблицу исключений.

Увлекательно было бы узнать, по крайней мере мне, как создатели JVM решились запихнуть оператор finally в компилятор взамен того, Дабы встроить его в виртуальную машину. Видимо, что выполнение этой работы компилятором разрешает гораздо упростить виртуальную машину, впрочем делает жизнь немножко труднее для людей как бы нас, которые создают иной язык для JVM.

Нетрадиционные примеры

Осознавание того, как реализован компилятор, помогает проще разбираться в некоторых нестандартных случаях. Скажем:

try {
  return "foo";
} finally {
  return "bar";
}

Итог будет «bar», потому что оператор finally будет подставлен перед оператором return, а значит return из finally-блока будет вызван первым, а return из try-блока вообще не будет вызван.

String value = "foo";
try {
  return value;
} finally {
  value = "bar";
}

Итог будет «foo», потому что значение для оператора return будет положено в стек до того, как будет вызван оператор finally, позже которого оно будет восстановлено и возвращено. (Мой пример это не показывает, но это именно то, что вы увидите, если посмотрите байт-код.) Таким образом, метаморфоза значения «value» в блоке finally не имеет ни какого значения для оператора return. И напоследок, что-то как бы:

while(true) {
  try {
    return "foo";
  } finally {
    break;
  }
}
return "bar";

Итог будет «bar». Это было изумлением даже для меня, впрочем все разумно, если вы знаете, что оператор break каждого лишь GOTO в байт-коде. Т.е. когда finally-блок подставляется как часть внутреннего оператора return, оператор GOTO вызывается прежде, чем инструкция RETURN, что приводит к выходу из цикла. (То же самое для оператора continue внутри finally-блока.)

Завершение

С нашей стороны, мы решили запретить операторы return, break и continue внутри finally-блоков из-за неопределенной семантики, как это сделано в C#. (И я Ощущаю, что собралась отличная компания из тех, кто принял такое решение.)

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

Глоссарий

подпрограмма = sub-routine
оператор = statement
подстановка = inlining
исключение = exception
внешняя функция = enhancement = extension function = mixin

Ссылки

[1] devblog.guidewire.com/author/akeefer/
[2] www.guidewire.com
[3] gosu-lang.org

Источник: programmingmaster.ru

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB