Статьи

Последнее обновление Oracle 8 для Java сломало ваши инструменты — как это случилось?

излом
Если вы в последнее время следили за новостями в мире Java, вы, вероятно, слышали, что последняя сборка Java 8, выпущенная Oracle, Java 8u11 (и Java 7u65), привела к ошибкам и сломала некоторые популярные сторонние инструменты, такие как JRebel, Javassist, Google Guice от ZeroTurnaround и даже Groovy.

Ошибки, выдаваемые JVM, длинные и подробные, но по сути они выглядят примерно так:

1
2
3
4
5
Exception in thread "main" java.lang.VerifyError: Bad method call from inside of a branch
Exception Details:
   Location:
   com/takipi/tests/dc/DepthCounter.()V @10: invokespecial
   

Причина, по которой эти ошибки неожиданно стали появляться, связана с тем, что верификатор байт-кода в последних обновлениях немного более строг, чем в предыдущих версиях. В отличие от предыдущих версий, он не позволяет вызывать супер-конструкторы из разветвленного кода.

Давайте разберемся с этим.

Java-байт-код и верификатор байт-кода

Байт-код является промежуточным языком, который фактически выполняет JVM и на котором записаны скомпилированные файлы .class . Машинный код JVM, если хотите.

Все языки на основе JVM компилируются в байт-код, от Java, до Scala, Groovy, Clojure и так далее. JVM не знает и не заботится о том, каким был исходный язык — он знает только байт-код.

Я не буду вдаваться в подробности того , как работает байт-код , поскольку это тема, достойная отдельной публикации (или нескольких сообщений), а просто для того, чтобы понять, как выглядит байт-код — возьмем, к примеру, этот простой Java-метод:

1
2
3
4
int add(int x, int y) {
   int z = x + y;
   return z;
}

При компиляции его байт-код выглядит так:

1
2
3
4
5
6
ILOAD x
ILOAD y
IADD
ISTORE z
ILOAD z
IRETURN

Когда JVM загружает файл класса из пути к классам в память, он должен сначала убедиться, что байт-код действителен и что код структурирован правильно. Он в основном проверяет, действительно ли загружаемый код может быть выполнен. Если байт-код исправен, класс успешно загружен в память; в противном случае выдается ошибка VerifyError , как и в начале сообщения.

Этот процесс называется проверкой байт-кода, и часть JVM, отвечающая за него, является верификатором байт-кода.

Почему это сломалось?

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

Одним из таких хорошо известных ограничений в языке Java является то, что самое первое, что вы должны сделать в конструкторе, прежде чем делать что-либо еще, — это вызвать super (…) или this (…) . Любой кусок кода до этого — и ваш код не скомпилируется. Даже если вы явно не пишете super () , компилятор неявно вставляет его для вас в самом начале конструктора.

Такое же ограничение существует, по крайней мере, на бумаге, в правилах проверки байт-кода . Однако оказывается, что до этих последних обновлений JDK это ограничение не было полностью применено. Это означает, что хотя ни один компилятор Java никогда не позволил бы вам скомпилировать этот код:

1
2
3
4
5
6
7
8
9
public static class ClassyClass {
   public ClassyClass() {
      if (checkSomething()) {
         super();
      } else {
         super(getSomething());
      }
   }
}

… Эквивалентный байт-код прошел бы проверку!

1
2
3
4
5
6
7
8
ALOAD this
    INVOKESTATIC checkSomething() : boolean
    IFEQ L2
    INVOKESPECIAL super() : void
    GOTO L2
L1: INVOKESTATIC getSomething() : int
    INVOKESPECIAL super(int) : void
L2: RETURN

Вы можете видеть в упрощенном байт-коде выше, что есть и вызов ( INVOKESTATIC ), и даже ветвь ( IFEQ — «если равно»), выполняемая перед первым вызовом супер-конструктора ( INVOKESPECIAL ).

Имейте в виду, что, хотя приведенный выше код не является допустимой Java, и, следовательно, ни один компилятор Java никогда не будет генерировать эквивалентный байт-код — существует множество других потенциально возможных инструментов, таких как компиляторы других языков JVM, которые не следуют ограничениям Java, и многие другие инструменты, такие как библиотеки инструментария байт-кода. Возможность выполнения кода перед вызовом super может быть довольно полезной!

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

С одной стороны, новый верификатор лоялен к спецификации, гарантируя, что наши JVM защищены от плохого кода. С другой стороны, многие инструменты, которые используют инструментарий байт-кода, такие как отладчики и ткачи аспекта (AOP), часто используют конструкции, подобные приведенным выше.

Как это решить?

Исправление для верификатора байт-кода уже зафиксировано , но еще не выпущено. Однако многие из затронутых инструментов и проектов уже выпускают исправленные версии и обходные пути.

Тем временем, если вы столкнулись с одной из этих ошибок, вы можете попробовать запустить JVM с аргументом командной строки -noverify . Эта опция указывает JVM пропускать проверку байт-кода при загрузке классов.