Зачем понимать ClassLoader
- Правильная работа плагинов, модулей, SPI/ServiceLoader, DI-фреймворков.
- Диагностика ClassNotFoundException / NoClassDefFoundError / LinkageError.
- Изоляция зависимостей в серверах приложений, OSGi, агентировании, hot-reload.
- Осознанная работа с classpath/module-path и
getResource*.
Иерархия загрузчиков и модель делегирования
Основные загрузчики (JDK 9+)
- Bootstrap ClassLoader
— Нативный (на C++), грузит «ядро» JDK (java.baseи др.). В Java представлен какnull. - Platform ClassLoader (ранее ExtensionClassLoader до Java 8)
— Грузит платформенные модули JDK (не базу), расширения. - Application (System) ClassLoader
— Грузит классы приложения с classpath (или module-path) — ваши JAR’ы/классы.
Проверить в рантайме:
ClassLoader app = ClassLoader.getSystemClassLoader();
ClassLoader platform = app.getParent();
ClassLoader bootstrap = platform.getParent(); // null → это Bootstrap
System.out.println(app); // Application
System.out.println(platform); // Platform
System.out.println(bootstrap); // nullМодель делегирования (parent delegation)
Алгоритм loadClass(name) по умолчанию:
- Делегировать родителю (
parent.loadClass(name)). - Если родитель не нашёл — пытаться найти самому (
findClass(name)).
Это предотвращает подмену базовых классов ядра: ваши классы не «перебьют» java.lang.String.
Пути загрузки: classpath vs module-path
- Classpath: до Java 8 (и сейчас тоже) — список каталогов/JAR’ов.
Опция:-cp/CLASSPATH. - Module-path (Java 9+): модульная система JPMS.
Опция:--module-path. Контроль экспорта пакетов (exports) и открытия для рефлексии (opens).
Если класс «виден» на module-path, но пакет не экспортирован модулем — InaccessibleObjectException/IllegalAccess при рефлексии, или ClassNotFoundException при разрешении зависимостей модулей.
Фазы жизненного цикла класса в JVM
- Loading — поиск байткода и создание
Class<?>черезdefineClass. - Linking = Verify (верификация байткода) + Prepare (выделение/иниц. статических полей нулями) + Resolve (разрешение ссылок на другие типы при обращении).
- Initialization — выполнение
<clinit>: статические инициализаторы/статические поля со значениями.
Инициализация запускается при первом активном использовании: new, обращение к static полю/методу и т.п.
Thread Context ClassLoader (TCCL)
TCCL (Thread.currentThread().getContextClassLoader()) — «контекстный» загрузчик, который фреймворки используют для поиска классов/ресурсов «со стороны приложения», а не «сверху».
Применение:
- ServiceLoader, XML-парсеры, логгеры, JDBC-драйверы, SPI.
- В контейнерах (например, сервлет-контейнер) контейнер выставляет TCCL на ClassLoader веб-приложения.
Пример:
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
URL res = tccl.getResource("config/app.yml");Поиск ресурсов: getResource vs getResourceAsStream
SomeClass.class.getResource("x.txt")— путь относительно пакетаSomeClass.SomeClass.class.getResource("/x.txt")— путь от корня classpath.ClassLoader.getResource("x.txt")— всегда от корня.
Надёжный паттерн:
try (var in = Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream("config/app.yml")) {
// читать поток
}Кастомный ClassLoader: когда и как
Когда это нужно:
- Плагинная система / «tenant-ы» с разными версиями библиотек.
- Загрузка классов из БД/сети/зашифрованных JAR’ов.
- Изоляция зависимостей, hot-reload.
Минимальный пример (правильная делегация родителю и собственный findClass):
public class ByteArrayClassLoader extends ClassLoader {
private final Map<String, byte[]> defs;
public ByteArrayClassLoader(ClassLoader parent, Map<String, byte[]> defs) {
super(parent); // делегирование родителю
this.defs = defs;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = defs.get(name);
if (bytes == null) throw new ClassNotFoundException(name);
return defineClass(name, bytes, 0, bytes.length);
}
}
// Использование:
Map<String, byte[]> code = Map.of("demo.Hello", compileBytes(...));
ClassLoader parent = Thread.currentThread().getContextClassLoader();
Class<?> helloCls = new ByteArrayClassLoader(parent, code).loadClass("demo.Hello");
Object hello = helloCls.getDeclaredConstructor().newInstance();Никогда не ломай делегирование «сверху вниз» без очень веской причины: легко получить «ад-класслодеров».
Частые ошибки и как их отличать
| Симптом | Когда возникает | Как думать |
|---|---|---|
| ClassNotFoundException | Не нашли класс при загрузке по имени (Class.forName, loadClass) | Нет на classpath/module-path, другой загрузчик, TCCL неправильный |
| NoClassDefFoundError | Класс был найден ранее, но теперь JVM не может его инициализировать/найти при ссылке | Инициализация упала (ExceptionInInitializerError), или класс исчез/не виден из этого загрузчика |
LinkageError (NoSuchMethodError, IncompatibleClassChangeError, ClassFormatError) | Конфликт версий класса/JAR, несовместимость сигнатур | Две версии одного класса в разных загрузчиках, “JAR hell” |
| ClassCastException «между одинаковыми именами» | Тот же класс загружен разными ClassLoader’ами | В JVM тип = {className, classLoader}. Два одинаковых имени — разные типы |
| Диагностика: |
System.out.println(obj.getClass().getClassLoader());
// или
System.out.println(SomeClass.class.getClassLoader());Контейнеры и изоляция (Tomcat/Jetty/Spring Boot)
- У каждого веб-приложения — свой ClassLoader. Это позволяет держать разные версии библиотек на одном сервере.
- Контейнер выставляет TCCL на ClassLoader веб-приложения перед вызовом сервлета/фильтра.
- Многие SPI (JDBC-драйверы, JAXB, JAXP, логгеры) ищут классы через TCCL. Если TCCL не выставлен — ловим
ClassNotFoundException.
Fat JAR (Spring Boot):
- Внутри один «лаунчер» с собственным резолвером JAR-ов → свой ClassLoader-стек.
- При отладке классов “из JAR внутри JAR” обращай внимание на
LaunchedURLClassLoader.
Модульная система (Java 9+) и доступ через рефлексию
exports: делает пакет видимым для компиляции/линковки другим модулям.opens: открывает пакет для рефлексии (например, Hibernate, Jackson).- Опции JVM для обхода:
--add-opens module/package=targetModule(илиALL-UNNAMEDдля немодульного мира). - В противном случае:
InaccessibleObjectException/IllegalAccessExceptionприsetAccessible(true)даже если класс есть на пути.
Service Provider Interface (SPI) и ServiceLoader
public interface PaymentGateway { void pay(); }
// META-INF/services/com.example.PaymentGateway
// содержит строку с FQN реализации: com.example.impl.StripeGateway
ServiceLoader<PaymentGateway> loader =
ServiceLoader.load(PaymentGateway.class, Thread.currentThread().getContextClassLoader());
for (PaymentGateway gw : loader) {
gw.pay();
}⚠️Важен TCCL — иначе ServiceLoader может «не увидеть» поставщиков из вашего ClassLoader.
Best practices (чек-лист)
- Соблюдай делегирование вверх: переопределяй
findClass, а неloadClass(если точно не знаешь что делаешь). - Логируй ClassLoader при ошибках: где искали класс/ресурс.
- Работай через TCCL в фреймворках/SPI:
Thread.currentThread().getContextClassLoader(). - Диагностика конфликтов: проверь, не загружается ли один и тот же класс разными загрузчиками.
- Resource-loading:
ClassLoader.getResource*— путь от корня;Class#getResource*— учитывает пакет. - Модули: планируй
exports/opensили JVM-флаги--add-opensдля рефлексии. - Не клади разные версии одного JAR на один путь: «JAR hell» → LinkageError.
- Закрывай
URLClassLoader(close()), если создаёшь временные загрузчики (горячая перезагрузка/плагины), иначе утечки. - В Spring Boot учитывай кастомный загрузчик лаунчера; для нативного теста выбрасывай «fat-jar» на классический classpath (профиль
repackage=false). - Изоляция плагинов: отдельный ClassLoader на плагин + родитель для API; минимизируй пересечение.
Мини-пример: диагностируем «класс есть, но не виден»
Симптом: NoClassDefFoundError: Lcom/x/Foo; при вызове кода библиотеки.
Пошагово:
System.out.println(Foo.class.getClassLoader());
// где лежит?
System.out.println(Foo.class.getProtectionDomain().getCodeSource().getLocation());
// кто вызывает?
for (var el : Thread.currentThread().getStackTrace())
System.out.println(el);
// проверь TCCL
System.out.println(Thread.currentThread().getContextClassLoader());
// проверь дубли
Enumeration<URL> urls =
Thread.currentThread().getContextClassLoader().getResources("com/x/Foo.class");
while (urls.hasMoreElements()) System.out.println(urls.nextElement());Если несколько путей — есть дубликаты. Если разные загрузчики — конфликт изоляции.