RU | EN | DE

Зачем понимать ClassLoader

  • Правильная работа плагинов, модулей, SPI/ServiceLoader, DI-фреймворков.
  • Диагностика ClassNotFoundException / NoClassDefFoundError / LinkageError.
  • Изоляция зависимостей в серверах приложений, OSGi, агентировании, hot-reload.
  • Осознанная работа с classpath/module-path и getResource*.

Иерархия загрузчиков и модель делегирования

Основные загрузчики (JDK 9+)

  1. Bootstrap ClassLoader
    — Нативный (на C++), грузит «ядро» JDK (java.base и др.). В Java представлен как null.
  2. Platform ClassLoader (ранее ExtensionClassLoader до Java 8)
    — Грузит платформенные модули JDK (не базу), расширения.
  3. 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) по умолчанию:

  1. Делегировать родителю (parent.loadClass(name)).
  2. Если родитель не нашёл — пытаться найти самому (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

  1. Loading — поиск байткода и создание Class<?> через defineClass.
  2. Linking = Verify (верификация байткода) + Prepare (выделение/иниц. статических полей нулями) + Resolve (разрешение ссылок на другие типы при обращении).
  3. 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 (чек-лист)

  1. Соблюдай делегирование вверх: переопределяй findClass, а не loadClass (если точно не знаешь что делаешь).
  2. Логируй ClassLoader при ошибках: где искали класс/ресурс.
  3. Работай через TCCL в фреймворках/SPI: Thread.currentThread().getContextClassLoader().
  4. Диагностика конфликтов: проверь, не загружается ли один и тот же класс разными загрузчиками.
  5. Resource-loading: ClassLoader.getResource* — путь от корня; Class#getResource* — учитывает пакет.
  6. Модули: планируй exports/opens или JVM-флаги --add-opens для рефлексии.
  7. Не клади разные версии одного JAR на один путь: «JAR hell» → LinkageError.
  8. Закрывай URLClassLoader (close()), если создаёшь временные загрузчики (горячая перезагрузка/плагины), иначе утечки.
  9. В Spring Boot учитывай кастомный загрузчик лаунчера; для нативного теста выбрасывай «fat-jar» на классический classpath (профиль repackage=false).
  10. Изоляция плагинов: отдельный 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());

Если несколько путей — есть дубликаты. Если разные загрузчики — конфликт изоляции.