После обновления библиотеки JVM выдаёт VerifyError ещё до выполнения метода. Какой этап загрузки класса объясняет отказ?
Отказ объясняет этап верификации байткода в процессе связывания класса. JVM проверяет, что class-файл структурно корректен, инструкции согласованы с типами, а переходы управления не нарушают правила работы операндного стека и локальных переменных. Если проверка не пройдена, выполнение метода не начинается, а JVM обычно выбрасывает VerifyError.
Верификация появилась как способ безопасно выполнять байткод, полученный не только от доверенного компилятора, но и из внешних источников. JVM должна была обнаруживать некорректные или потенциально опасные последовательности инструкций до их исполнения, особенно в средах с загрузкой кода через сеть.
Этот подход также отделяет ответственность JVM от компилятора: даже корректность исходного Java-кода не означает, что любой сгенерированный или преобразованный class-файл можно безопасно выполнить.
Ошибка часто возникает после обновления библиотек генерации или инструментов трансформации байткода: прокси, агентов, мок-фреймворков, компиляторов языков JVM. Инструмент может создать class-файл с несовместимыми типами на стеке, неверными метаданными переходов или нарушенными ограничениями формата.
Неверно считать такую ошибку обычной проблемой отсутствующей зависимости. При отсутствии класса обычно возникают другие ошибки связывания, например ClassNotFoundException или NoClassDefFoundError. При VerifyError класс или метод не соответствует правилам верификатора, даже если необходимые зависимости доступны.
После загрузки байтов класса JVM выполняет этапы связывания: проверку, подготовку и, при необходимости, разрешение символических ссылок. Точный момент проверки может быть отложен реализацией JVM, поэтому ошибка иногда появляется не в момент чтения class-файла, а при первом связывании конкретного метода или инструкции.
Верификатор проверяет несколько групп условий:
Для современных версий class-файлов важную роль играют StackMapTable и вычисляемые по ней кадры типов. Они позволяют JVM проверять типовое состояние в точках ветвления без полного дорогостоящего вывода типов по всем возможным путям. Некорректные или несогласованные кадры могут привести к VerifyError.
Верификация не равна проверке всех прав доступа и не является полной защитой от вредоносного поведения. Проверки доступа, политика модулей, загрузчики классов и ограничения среды выполняют отдельные функции. Кроме того, код с нативными методами выходит за пределы гарантий, которые даёт проверка Java-байткода.
Отключение или ослабление проверки не является нормальным способом исправления проблемы: это снижает безопасность и может не устранить несовместимость. Надёжное решение — исправить генератор или трансформатор байткода, согласовать его версию с целевой JVM и проверять результат в CI на поддерживаемых версиях Java.
Java-агент изменял методы приложения, добавляя измерение времени. После обновления библиотеки генерации байткода приложение стало получать VerifyError при загрузке отдельных классов. Причиной оказалось преобразование метода с ветвлением без корректного описания типов в точке слияния ветвей.
Рассматривались три варианта:
Выбрали третий вариант: агент перевели на совместимую версию библиотеки, добавили проверку сгенерированных классов на всех поддерживаемых JDK и отдельные тесты для методов с ветвлениями. В результате ошибка исчезла без ослабления проверок JVM.
Нет. Обычный компилятор обычно генерирует корректный байткод, но class-файл может быть изменён агентом, создан генератором прокси или скомпилирован другим JVM-языком. Поэтому успешная компиляция исходников не гарантирует корректность финального байткода, который получает JVM.
Нет. Связывание и проверка могут выполняться лениво. Класс может быть прочитан, а проверка конкретного метода или разрешение его ссылок произойдёт только при первом обращении к нему. Поэтому место возникновения ошибки в стеке вызовов не всегда совпадает с моментом появления дефекта в сгенерированном class-файле.
Нет. JIT компилирует только байткод, который уже принят интерпретатором и проверками JVM. Верификатор является предварительным барьером корректности; машинный код не должен использоваться для обхода этих ограничений. Если байткод не прошёл проверку, до JIT-компиляции соответствующего метода дело не дойдёт.