Программа собралась, тесты прошли, на сервере запускается — и падает с
NoClassDefFoundError на классе, который лежит в проекте и прекрасно
компилировался. Или наоборот: две библиотеки принесли один и тот же класс,
и приложение сообщает, что Order нельзя привести к Order.
Оба случая — про то, как виртуальная машина находит классы и что она считает «тем же самым классом». Разберём это по порядку: что происходит в момент первого обращения к классу, кто именно его ищет и почему одинаковые на вид имена оказываются разными типами.
Класс попадает в память не при запуске, а при первом обращении
Когда вы запускаете java -jar app.jar, виртуальная машина не читает все классы
подряд. Она берёт класс с методом main, а дальше подтягивает остальные по мере
необходимости — как только до них доходит дело.
«Дошло дело» означает конкретные вещи: создали объект через new, обратились к
статическому полю или методу, вызвали класс через отражение, обратились к
наследнику (сначала загрузится родитель). Пока ни одного такого обращения не
случилось, класса в памяти нет.
Проверить это можно статическим блоком — он выполняется ровно один раз, при подготовке класса к работе:
live example
public class Main {
static class Config {
static { System.out.println("Config готов"); }
static final String NAME = "orders";
static int counter = 0;
}
public static void main(String[] args) {
System.out.println("старт");
System.out.println(Config.NAME); // константа — класс НЕ загрузится
System.out.println(Config.counter); // обычное поле — вот теперь загрузится
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Порядок вывода: старт, orders, Config готов, 0. Строка orders печатается
до сообщения из статического блока, и это не опечатка: static final со
значением, известным на этапе компиляции, компилятор подставляет прямо в место
использования. Обращения к классу не остаётся, загружать нечего.
Три этапа: найти, связать, выполнить статику
Путь класса от файла до готового к работе типа состоит из трёх шагов.
Загрузка — найти байты .class и создать в памяти объект типа Class. Где
именно искать, решает загрузчик (о них ниже).
Связывание — тут три отдельных дела. Проверка (verification): байт-код
читается и проверяется на корректность — это защита от испорченного или чужого
файла, а не формальность. Подготовка (preparation): статическим полям
выделяется память и проставляются значения по умолчанию — ноль, false, null.
Разрешение ссылок (resolution): символические ссылки на другие классы и методы
заменяются прямыми; часть этой работы виртуальная машина откладывает до первого
реального обращения.
Инициализация — выполняются статические блоки и присваивания статическим полям, сверху вниз по тексту класса. Это тот самый момент, когда печатается «Config готов».
Разница между подготовкой и инициализацией видна на примере: после подготовки
counter равен нулю, потому что так устроен int, а после инициализации —
значению из кода. Если статический блок бросит исключение, класс останется
битым навсегда: первое обращение даст ExceptionInInitializerError, а все
последующие — NoClassDefFoundError с тем же именем. Отсюда частая путаница:
ошибка выглядит как «класса нет», а на самом деле он есть, просто его статика
однажды упала.
Кто ищет классы: три загрузчика и делегирование
Загрузчиков в обычном приложении три, и они выстроены цепочкой.
- Bootstrap — встроенный в виртуальную машину, написан не на Java. Загружает
сам язык:
java.lang,java.util, всё ядро платформы. В коде он представлен какnull— это не ошибка, а соглашение. - Platform — остальные модули платформы, те, что не в ядре. До Java 9 назывался extension.
- Application (он же system) — ваш код и библиотеки, то есть всё, что перечислено в classpath или на пути модулей.
Работают они по правилу делегирования вверх: прежде чем искать класс самому, загрузчик просит найти его родителя, и только если тот не нашёл — ищет сам.
live example
public class Loaders {
public static void main(String[] args) {
System.out.println(String.class.getClassLoader()); // null — bootstrap
System.out.println(Loaders.class.getClassLoader()); // app
System.out.println(Loaders.class.getClassLoader().getParent()); // platform
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Смысл правила — безопасность и предсказуемость. Положите в свой проект класс
java.lang.String — он никогда не будет загружен: запрос уйдёт вверх, и
bootstrap отдаст настоящий. Подменить класс платформы своим нельзя, и это
сознательное ограничение.
Класс — это имя плюс загрузчик
Здесь начинается то, из-за чего появляется странное «Order нельзя привести к
Order».
Виртуальная машина считает два класса одним и тем же, только если совпадает и
полное имя, и загрузчик. Один и тот же файл, загруженный двумя разными
загрузчиками, даёт два разных типа. Объект, созданный первым, нельзя присвоить
переменной второго — будет ClassCastException, а в сообщении оба имени
выглядят одинаково.
Встречается это там, где загрузчиков больше одного: сервер приложений с
несколькими развёрнутыми приложениями, система плагинов, перезагрузка кода на
лету. Признак в логе всегда один: ClassCastException или IllegalArgumentException
с одинаковыми именами по обе стороны — значит, дело не в типах, а в загрузчиках.
ClassNotFoundException и NoClassDefFoundError
Два имени, которые постоянно путают. Разница простая, если помнить, кто спрашивал.
ClassNotFoundException — обычное проверяемое исключение. Его бросают, когда
класс ищут по имени в строке: Class.forName("com.example.Order"),
loadClass, поиск драйвера. Виртуальная машина честно сообщает: такого имени
нет ни у одного загрузчика в цепочке. Обычно это опечатка в имени или
отсутствующая зависимость, о которой код знал заранее и потому обёрнут в
try/catch.
NoClassDefFoundError — ошибка, а не исключение. Она означает: при компиляции
класс был, при запуске его нет. Никто не спрашивал класс по строке, на него
просто ссылался код. Причины на практике три:
- в сборку не попала библиотека — зависимость помечена как доступная только на компиляцию или во время тестов;
- версия библиотеки в проде другая, и в ней этого класса уже (или ещё) нет;
- класс на месте, но его статика однажды упала — тот самый случай из раздела про инициализацию.
Различить второе и третье помогает первое появление ошибки в логах: если самым
первым был ExceptionInInitializerError, ищите причину в статическом блоке, а не
в сборке.
Где это встречается в обычной работе
Приложение одним архивом. Обычный jar не умеет читать вложенные архивы, а
собранное приложение Spring Boot — это как раз jar с jar-ами внутри. Поэтому
фреймворк добавляет свой загрузчик, который умеет доставать классы из вложенных
архивов, и подменяет им стандартный. Если вы распаковали такой архив и
запустили классы напрямую — ничего не найдётся, хотя файлы на месте.
Плагины и модули. Каждый плагин грузят своим загрузчиком: так его классы не мешают соседям, а сам плагин можно выгрузить. Общий контракт при этом кладут в родительский загрузчик — иначе интерфейс из плагина и интерфейс из ядра окажутся разными типами.
Веб-контейнеры. Tomcat и подобные ему нарушают правило делегирования намеренно: загрузчик приложения сначала ищет класс у себя и только потом идёт к родителю. Так приложение может принести свою версию библиотеки, не спрашивая сервер. Обратная сторона — конфликты версий, которые проявляются только в контейнере.
Утечки памяти. Класс выгружается только вместе со своим загрузчиком, а
загрузчик жив, пока на него есть ссылка. Достаточно одной статической ссылки из
класса платформы или незачищенного ThreadLocal в пуле потоков — и при каждой
перезагрузке приложения в памяти остаётся ещё одна копия всех его классов.
Симптом — растущий Metaspace и OutOfMemoryError: Metaspace после нескольких
перезагрузок без перезапуска процесса.
Коротко
- Класс загружается не при старте, а при первом реальном обращении к нему;
static finalконстанты обращения не создают. - Путь класса: загрузка → связывание (проверка, подготовка, разрешение ссылок) → инициализация со статическими блоками.
- Загрузчиков три — bootstrap (ядро платформы, виден как
null), platform и application; каждый сначала делегирует поиск родителю. - Класс определяется парой «полное имя + загрузчик»: один файл в двух загрузчиках даёт два несовместимых типа.
ClassNotFoundException— класс искали по строке и не нашли;NoClassDefFoundError— он был при компиляции, но не нашёлся при запуске либо упал в статическом блоке.- Классы выгружаются вместе с загрузчиком: живая ссылка на загрузчик — это утечка всего его кода в Metaspace.
Что почитать дальше
- Сборка мусора в Java — как освобождается память объектов и почему Metaspace живёт по своим правилам.
- Исключения — чем ошибки отличаются от проверяемых исключений и почему
Errorне ловят по месту. - Инструменты разработчика Java — как собрать приложение так, чтобы нужные библиотеки оказались в архиве.
- ООП в Java — типы, наследование и приведение, с которыми и связаны конфликты загрузчиков.