ClassLoader’ы: делегирование

ClassLoader’ы: делегирование

  • Делегирование «снизу вверх»: сначала родитель, потом текущий лоадер.

  • В Java 9+ Extension ClassLoader заменён на Platform ClassLoader.

  • String.class.getClassLoader() возвращает null (загружен Bootstrap’ом) — это не ошибка.


    1) Что делает ClassLoader — коротко и правильно

    ClassLoader — это часть JVM, которая по запросу находит байткод класса (обычно в .class/JAR, а в Java 9+ ещё и в модульном jimage), валидирует его, связывает с остальной программой и при необходимости запускает инициализацию класса (статические блоки/поля).

    Фазы загрузки (упрощённо и в корректной терминологии JVM):

    1. Loading — поиск и чтение байткода, создание объекта java.lang.Class.

    2. Linking — «подружить» класс с VM:

      • Verification — проверка корректности/безопасности байткода.

      • Preparation — выделение памяти под static-поля и их значения по умолчанию.

      • (Optional) Resolution — преобразование символьных ссылок на поля/методы/классы в прямые ссылки (часто лениво, при первом обращении).

    3. Initialization — выполнение статических инициализаторов и присвоение начальных значений static-полям (не «по умолчанию», а именно заданных в коде).

    Class.forName(name) по умолчанию загружает и инициализирует класс.
    Class.forName(name, false, loader) или loader.loadClass(name) — загружают (и линкуют), но не инициализируют до первого «активационного» использования.


    2) Встроенные загрузчики в Java 9+ (JPMS)

    С приходом JPMS (модульной системы) важны три уровня:

    • Bootstrap Class Loader
      Встроен в JVM (нативный), грузит основные модули JDK (в т.ч. java.base) из образа lib/modules (формат jimage). Объекта ClassLoader у него нет — потому String.class.getClassLoader() даёт null.

    • Platform Class Loader (заменил Extension с Java 9)
      Загружает «платформенные» модули JDK/поставщика (то, что не в java.base). Получить: ClassLoader.getPlatformClassLoader().

    • Application (System) Class Loader
      Грузит классы/модули приложения из --class-path или --module-path. Доступ: ClassLoader.getSystemClassLoader().

    Extension ClassLoader (каталог jre/lib/ext) — исторический артефакт до Java 8. В Java 9+ его роль занял Platform ClassLoader.

    Делегирование

    По умолчанию — parent-first: при попытке загрузить класс сначала спрашиваем родителя, и только если тот «не знает», загружаем сами. Это не только ускоряет работу за счёт кешей на верхних уровнях, но и повышает безопасность/предсказуемость (ядро платформы всегда одно и то же).


    3) ClassPath, ModulePath и слои модулей

    • Классическая схема: --class-path (или CLASSPATH) → классы/JAR’ы.

    • Модульная схема (Java 9+): --module-path + module-info.java.

    • Приложение всегда стартует в boot layer модульной системы; дополнительно можно создавать динамические слои (ModuleLayer) для изолированных плагинов.


    4) Контекстный загрузчик (TCCL)

    Многие фреймворки/библиотеки используют Thread Context ClassLoader:




    ClassLoader tccl = Thread.currentThread().getContextClassLoader();

    Это позволяет библиотеке, работающей под своим загрузчиком, находить классы вашего приложения (например, при SPI, ServiceLoader, загрузке обработчиков и т.п.).


    5) Пользовательские ClassLoader’ы: когда и как

    Зачем:

    • Плагин-системы (изолировать зависимости плагинов).

    • «Горячая» перезагрузка классов/многоверсионная загрузка.

    • Загрузка из нестандартных источников (БД, сеть, шифрованные контейнеры).

    • Песочницы (sandbox), частичный контроль доступа (исторически — вместе с Security Manager; ныне он депрекейтед к удалению, потому «песочность» достигается в основном изоляцией слоёв/процессов).

    Минимальный скелет (parent-first, корректно переопределяем findClass):




    public class BytesClassLoader extends ClassLoader { public BytesClassLoader(ClassLoader parent) { super(parent); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] bytes = fetchBytesFor(name); // ваш источник: файл/сеть/шифрованный архив if (bytes == null) throw new ClassNotFoundException(name); return defineClass(name, bytes, 0, bytes.length); } }

    Child-first (редко, но бывает нужно) — специально нарушаем делегирование для «затенения» версии класса из родителя (плагины/OSGi-подобные кейсы). Делать осторожно, чтобы не поймать LinkageError из-за смешения разных Class с одинаковыми именами.


    6) Диагностика проблем загрузки

    Частые исключения/ошибки:

    • ClassNotFoundException — класс не найден загрузчиком (часто: неправильный путь/модуль).

    • NoClassDefFoundError — класс был виден при компиляции, но не найден при выполнении (разошлись classpath/modulepath, «теневой» класс, удалённый JAR).

    • UnsupportedClassVersionError — версия байткода новее, чем поддерживает JVM.

    • IncompatibleClassChangeError / NoSuchMethodError / NoSuchFieldError — бинарная несовместимость (подменили JAR другой версией).

    • LinkageError — общая надкласс-ошибка при конфликте определений класса.

    Как дебажить:

    • Для модулей: запускайте с --show-module-resolution, смотрите, что реально подхватилось.

    • Для classpath: -verbose:class покажет, откуда каждый класс загружен.

    • Явно печатайте cl.getResource("pkg/Class.class"), чтобы увидеть, какой ClassLoader и какой URL.


    7) Сервисы и модульность (SPI)

    Современный способ объявить и найти реализации — ServiceLoader.
    В модульном мире это ещё и директивы:




    // module-info.java module com.acme.app { uses com.acme.spi.Reporter; provides com.acme.spi.Reporter with com.acme.impl.ConsoleReporter; }

    В классическом мире сервисы ищутся по файлам META-INF/services/... и часто используют TCCL.


    8) Java-агенты и трансформация байткода

    Через -javaagent:agent.jar можно перехватывать загрузку классов и трансформировать байткод (профилирование, APM, трассировка):




    public class Agent { public static void premain(String args, Instrumentation inst) { inst.addTransformer((loader, name, classBeingRedefined, domain, bytes) -> { // вернуть модифицированный байткод или null return null; }); } }


    9) Практика: «что, где и как» — правила здорового проекта

    • Не мешайте classpath и modulepath: выбрали JPMS — завершите модульность целиком или чётко поймите границу «автомодулей».

    • Не перегружайте AppClassLoader динамическими путями — используйте URLClassLoader/кастомный загрузчик/динамические модульные слои.

    • Не используйте Class.forName(x).newInstance() — он устаревший. Предпочитайте Class.forName(x).getDeclaredConstructor().newInstance().

    • Соблюдайте «один класс — один загрузчик» в пределах взаимодействующей области: иначе поймаете ClassCastException между логически тем же самым типом, но загруженным другим ClassLoader.

    • Для GraalVM native-image заранее декларируйте всё, что грузите рефлексией/динамически (reflect-config), т.к. в статическом образе нет полноценной динамики загрузки.


    Маленькие, но важные примеры

    1) Разница Class.forName и loadClass:




    Class<?> a = Class.forName("com.example.A"); // загрузит + инициализирует Class<?> b = Class.forName("com.example.B", false, myLoader); // без инициализации Class<?> c = myLoader.loadClass("com.example.C"); // без инициализации

    2) Child-first для плагинов (осторожно!):




    public class ChildFirstURLClassLoader extends URLClassLoader { public ChildFirstURLClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1) сначала проверим: уже загружен? Class<?> c = findLoadedClass(name); if (c == null) { try { c = findClass(name); } // 2) пытаемся САМИ (child-first) catch (ClassNotFoundException ignore) { c = super.loadClass(name, false); } // 3) иначе родитель } if (resolve) resolveClass(c); return c; } } }

    3) Использование ServiceLoader (модульно):




    ServiceLoader<MySpi> loader = ServiceLoader.load(MySpi.class); for (MySpi impl : loader) { impl.run(); }


    Итого — что изменилось «в последних версиях»

    • Extension ClassLoader → Platform ClassLoader (с Java 9).

    • JPMS/модульность: классы ядра грузятся из образа модулей (jimage), у приложения появились module-path и модульные слои.

    • Сильная инкапсуляция JDK: доступ к внутренностям JDK без --add-opens/--add-exports ограничен (это не про загрузку, но влияет на «рефлексивную магию» при инициализации классов).

    • Security Manager объявлен устаревшим к удалению — не рассчитывайте на старые «песочницы»; делайте изоляцию через загрузчики/модули/процессы.

    • Появились виртуальные потоки (Java 21) — это про планирование задач, но на архитектуру загрузчиков не влияет.

    • В продакшене широко используются агенты/трансформеры и AOT/GraalVM: для последних нужно заранее описывать всё динамически загружаемое

      Bootstrap 

      1. Bootstrap ClassLoader 

      Первый и главный загрузчик классов, встроенный в JVM.

      Но:

      • В старых версиях (до Java 8) он загружал классы из файлов rt.jarresources.jarcharsets.jar и т.д.

      • В Java 9 и новее появилась модульная система (JPMS), и теперь Bootstrap работает не с JAR-файлами, а с модульным образом JDK (lib/modules, внутри — бинарный контейнер .jimage).

      То есть Bootstrap ClassLoader остался, но его источник классов полностью поменялся.


       2. Как теперь это устроено (начиная с Java 9)

      Было (до Java 8):



      <JAVA_HOME>/jre/lib/rt.jar <JAVA_HOME>/jre/lib/resources.jar

      Стало (Java 9+):


      <JAVA_HOME>/lib/modules

      Это специальный бинарный образ (jimage), где хранятся модули, включая:

      • java.base (всегда подключён, ядро JDK)

      • java.loggingjava.xmljava.desktop и т.д.

      Bootstrap теперь загружает классы из модульного слоя boot layer, а не напрямую из jar-файлов.


       3. Что он загружает

      • Все классы из модуля java.base — это базовые пакеты:


        java.lang.* java.util.* java.io.* java.nio.* java.math.* java.net.* java.time.*
      • Некоторые внутренние пакеты (jdk.internal.*) — тоже его зона ответственности.

      Пример:


      System.out.println(String.class.getClassLoader()); // null

      null здесь — не ошибка, а признак, что String загружен Bootstrap-загрузчиком (у него нет Java-объекта).


      4. Почему у него нет объекта ClassLoader

      Bootstrap ClassLoader написан на C/C++, встроен в ядро JVM.
      Он не является экземпляром java.lang.ClassLoader и даже не имеет Java-представления.
      Именно поэтому getClassLoader() для базовых классов возвращает null.

      Это сделано специально:

      • для скорости (загрузчик стартует вместе с JVM);

      • для безопасности — нельзя подменить или переопределить его логику.


      5. Почему его нельзя заменить / наследовать

      Причины:

      1. Безопасность: если злоумышленник сможет подменить загрузчик базовых классов (java.lang.Systemjava.security.* и т.д.) — вся платформа компрометируется.

      2. Стабильность: при инициализации JVM уже используются эти классы — Bootstrap должен быть «железно предсказуемым».

      3. Технически невозможно — он живёт в нативном коде JVM, не в Java-куче.


      6. Как Bootstrap работает внутри

      Он встроен в C++-часть JVM, вызывается на старте VM при инициализации модуля java.base.

      Примерно так:


      BootstrapClassLoader → загружает java.base → создаёт базовый модульный слой (boot layer) → инициализирует java.lang.Object, ClassLoader, Thread, System и др. → после этого активируется PlatformClassLoader


       7. Его место в иерархии

      Современная иерархия (Java 9+):


      flowchart TB A[Bootstrap ClassLoader<br>(нативный, загружает java.base)]:::core B[Platform ClassLoader<br>(загружает модули JDK, кроме java.base)]:::core C[Application ClassLoader<br>(загружает классы приложения из classpath)]:::core D[Custom ClassLoader<br>(плагины, шифрованные классы, hot-reload)]:::core A --> B --> C --> D classDef core fill:#f0f7ff,stroke:#4a74b4,stroke-width:1px,color:#0b234a


      8. Что изменилось после Java 9

      Было (до Java 8) Стало (Java 9+)
      Bootstrap грузил классы из rt.jar Теперь грузит из модуля java.base внутри lib/modules
      Extension ClassLoader искал jre/lib/ext Заменён на Platform ClassLoader
      rt.jartools.jar Удалены, заменены модульной структурой
      Можно было «подсунуть» JAR в bootstrap-путь через -Xbootclasspath Теперь для этого есть --patch-module--upgrade-module-path
      Все public-классы JDK были видны через classpath Теперь нужны exports / opens для доступа к ним



      9. Пример: демонстрация уровней загрузчиков


      public class LoaderDemo { public static void main(String[] args) { System.out.println(String.class.getClassLoader()); // null System.out.println(java.sql.Connection.class.getClassLoader()); // Platform System.out.println(LoaderDemo.class.getClassLoader()); // Application } }

      Вывод (в Java 17+):


      null PlatformClassLoader@4e25154f AppClassLoader@18b4aac2


       10. Для продвинутых: как JVM делегирует загрузку


      смотреть диаграмму




       11. Где увидеть «вживую»

      Запустите с флагом:


      java --list-modules

      Покажет все модули, загружаемые Bootstrap/Platform.

      Посмотреть путь загрузки:


      java -Xlog:class+load=info MyApp

      или старый вариант:


      java -verbose:class MyApp


      Ключевой вывод

      Bootstrap ClassLoader не устарел, а:

      • остался фундаментом загрузочной системы JVM;

      • просто перешёл с JAR на модульный формат (jimage);

      • работает полностью на нативном уровне;

      • отвечает за безопасность и консистентность платформы.

  • Автор: к.п.н., Румянцев Сергей Александрович, доцент Финансового университета при Правительстве РФ; доцент ОЧУВО Международного инновационного университета; Консалтинг, управление разработкой ПО; системный и бизнес анализ; менеджмент; аналитиз данных; управление ИТ. Телефон для связи +79269444818 (мессенджеры)   Короткая ссылка:
    ClassLoader’ы: делегирование

  • Делегирование «снизу вверх»: сначала родитель, потом текущий лоадер.

  • В Java 9+ Extension ClassLoader заменён на Platform ClassLoader.

  • String.class.getClassLoader() возвращает null (загружен Bootstrap’ом) — это не ошибка.


    1) Что делает ClassLoader — коротко и правильно

    ClassLoader — это часть JVM, которая по запросу находит байткод класса (обычно в .class/JAR, а в Java 9+ ещё и в модульном jimage), валидирует его, связывает с остальной программой и при необходимости запускает инициализацию класса (статические блоки/поля).

    Фазы загрузки (упрощённо и в корректной терминологии JVM):

    1. Loading — поиск и чтение байткода, создание объекта java.lang.Class.

    2. Linking — «подружить» класс с VM:

      • Verification — проверка корректности/безопасности байткода.

      • Preparation — выделение памяти под static-поля и их значения по умолчанию.

      • (Optional) Resolution — преобразование символьных ссылок на поля/методы/классы в прямые ссылки (часто лениво, при первом обращении).

    3. Initialization — выполнение статических инициализаторов и присвоение начальных значений static-полям (не «по умолчанию», а именно заданных в коде).

    Class.forName(name) по умолчанию загружает и инициализирует класс.
    Class.forName(name, false, loader) или loader.loadClass(name) — загружают (и линкуют), но не инициализируют до первого «активационного» использования.


    2) Встроенные загрузчики в Java 9+ (JPMS)

    С приходом JPMS (модульной системы) важны три уровня:

    • Bootstrap Class Loader
      Встроен в JVM (нативный), грузит основные модули JDK (в т.ч. java.base) из образа lib/modules (формат jimage). Объекта ClassLoader у него нет — потому String.class.getClassLoader() даёт null.

    • Platform Class Loader (заменил Extension с Java 9)
      Загружает «платформенные» модули JDK/поставщика (то, что не в java.base). Получить: ClassLoader.getPlatformClassLoader().

    • Application (System) Class Loader
      Грузит классы/модули приложения из --class-path или --module-path. Доступ: ClassLoader.getSystemClassLoader().

    Extension ClassLoader (каталог jre/lib/ext) — исторический артефакт до Java 8. В Java 9+ его роль занял Platform ClassLoader.

    Делегирование

    По умолчанию — parent-first: при попытке загрузить класс сначала спрашиваем родителя, и только если тот «не знает», загружаем сами. Это не только ускоряет работу за счёт кешей на верхних уровнях, но и повышает безопасность/предсказуемость (ядро платформы всегда одно и то же).


    3) ClassPath, ModulePath и слои модулей

    • Классическая схема: --class-path (или CLASSPATH) → классы/JAR’ы.

    • Модульная схема (Java 9+): --module-path + module-info.java.

    • Приложение всегда стартует в boot layer модульной системы; дополнительно можно создавать динамические слои (ModuleLayer) для изолированных плагинов.


    4) Контекстный загрузчик (TCCL)

    Многие фреймворки/библиотеки используют Thread Context ClassLoader:




    ClassLoader tccl = Thread.currentThread().getContextClassLoader();

    Это позволяет библиотеке, работающей под своим загрузчиком, находить классы вашего приложения (например, при SPI, ServiceLoader, загрузке обработчиков и т.п.).


    5) Пользовательские ClassLoader’ы: когда и как

    Зачем:

    • Плагин-системы (изолировать зависимости плагинов).

    • «Горячая» перезагрузка классов/многоверсионная загрузка.

    • Загрузка из нестандартных источников (БД, сеть, шифрованные контейнеры).

    • Песочницы (sandbox), частичный контроль доступа (исторически — вместе с Security Manager; ныне он депрекейтед к удалению, потому «песочность» достигается в основном изоляцией слоёв/процессов).

    Минимальный скелет (parent-first, корректно переопределяем findClass):




    public class BytesClassLoader extends ClassLoader { public BytesClassLoader(ClassLoader parent) { super(parent); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] bytes = fetchBytesFor(name); // ваш источник: файл/сеть/шифрованный архив if (bytes == null) throw new ClassNotFoundException(name); return defineClass(name, bytes, 0, bytes.length); } }

    Child-first (редко, но бывает нужно) — специально нарушаем делегирование для «затенения» версии класса из родителя (плагины/OSGi-подобные кейсы). Делать осторожно, чтобы не поймать LinkageError из-за смешения разных Class с одинаковыми именами.


    6) Диагностика проблем загрузки

    Частые исключения/ошибки:

    • ClassNotFoundException — класс не найден загрузчиком (часто: неправильный путь/модуль).

    • NoClassDefFoundError — класс был виден при компиляции, но не найден при выполнении (разошлись classpath/modulepath, «теневой» класс, удалённый JAR).

    • UnsupportedClassVersionError — версия байткода новее, чем поддерживает JVM.

    • IncompatibleClassChangeError / NoSuchMethodError / NoSuchFieldError — бинарная несовместимость (подменили JAR другой версией).

    • LinkageError — общая надкласс-ошибка при конфликте определений класса.

    Как дебажить:

    • Для модулей: запускайте с --show-module-resolution, смотрите, что реально подхватилось.

    • Для classpath: -verbose:class покажет, откуда каждый класс загружен.

    • Явно печатайте cl.getResource("pkg/Class.class"), чтобы увидеть, какой ClassLoader и какой URL.


    7) Сервисы и модульность (SPI)

    Современный способ объявить и найти реализации — ServiceLoader.
    В модульном мире это ещё и директивы:




    // module-info.java module com.acme.app { uses com.acme.spi.Reporter; provides com.acme.spi.Reporter with com.acme.impl.ConsoleReporter; }

    В классическом мире сервисы ищутся по файлам META-INF/services/... и часто используют TCCL.


    8) Java-агенты и трансформация байткода

    Через -javaagent:agent.jar можно перехватывать загрузку классов и трансформировать байткод (профилирование, APM, трассировка):




    public class Agent { public static void premain(String args, Instrumentation inst) { inst.addTransformer((loader, name, classBeingRedefined, domain, bytes) -> { // вернуть модифицированный байткод или null return null; }); } }


    9) Практика: «что, где и как» — правила здорового проекта

    • Не мешайте classpath и modulepath: выбрали JPMS — завершите модульность целиком или чётко поймите границу «автомодулей».

    • Не перегружайте AppClassLoader динамическими путями — используйте URLClassLoader/кастомный загрузчик/динамические модульные слои.

    • Не используйте Class.forName(x).newInstance() — он устаревший. Предпочитайте Class.forName(x).getDeclaredConstructor().newInstance().

    • Соблюдайте «один класс — один загрузчик» в пределах взаимодействующей области: иначе поймаете ClassCastException между логически тем же самым типом, но загруженным другим ClassLoader.

    • Для GraalVM native-image заранее декларируйте всё, что грузите рефлексией/динамически (reflect-config), т.к. в статическом образе нет полноценной динамики загрузки.


    Маленькие, но важные примеры

    1) Разница Class.forName и loadClass:




    Class<?> a = Class.forName("com.example.A"); // загрузит + инициализирует Class<?> b = Class.forName("com.example.B", false, myLoader); // без инициализации Class<?> c = myLoader.loadClass("com.example.C"); // без инициализации

    2) Child-first для плагинов (осторожно!):




    public class ChildFirstURLClassLoader extends URLClassLoader { public ChildFirstURLClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1) сначала проверим: уже загружен? Class<?> c = findLoadedClass(name); if (c == null) { try { c = findClass(name); } // 2) пытаемся САМИ (child-first) catch (ClassNotFoundException ignore) { c = super.loadClass(name, false); } // 3) иначе родитель } if (resolve) resolveClass(c); return c; } } }

    3) Использование ServiceLoader (модульно):




    ServiceLoader<MySpi> loader = ServiceLoader.load(MySpi.class); for (MySpi impl : loader) { impl.run(); }


    Итого — что изменилось «в последних версиях»

    • Extension ClassLoader → Platform ClassLoader (с Java 9).

    • JPMS/модульность: классы ядра грузятся из образа модулей (jimage), у приложения появились module-path и модульные слои.

    • Сильная инкапсуляция JDK: доступ к внутренностям JDK без --add-opens/--add-exports ограничен (это не про загрузку, но влияет на «рефлексивную магию» при инициализации классов).

    • Security Manager объявлен устаревшим к удалению — не рассчитывайте на старые «песочницы»; делайте изоляцию через загрузчики/модули/процессы.

    • Появились виртуальные потоки (Java 21) — это про планирование задач, но на архитектуру загрузчиков не влияет.

    • В продакшене широко используются агенты/трансформеры и AOT/GraalVM: для последних нужно заранее описывать всё динамически загружаемое

      Bootstrap 

      1. Bootstrap ClassLoader 

      Первый и главный загрузчик классов, встроенный в JVM.

      Но:

      • В старых версиях (до Java 8) он загружал классы из файлов rt.jarresources.jarcharsets.jar и т.д.

      • В Java 9 и новее появилась модульная система (JPMS), и теперь Bootstrap работает не с JAR-файлами, а с модульным образом JDK (lib/modules, внутри — бинарный контейнер .jimage).

      То есть Bootstrap ClassLoader остался, но его источник классов полностью поменялся.


       2. Как теперь это устроено (начиная с Java 9)

      Было (до Java 8):



      <JAVA_HOME>/jre/lib/rt.jar <JAVA_HOME>/jre/lib/resources.jar

      Стало (Java 9+):


      <JAVA_HOME>/lib/modules

      Это специальный бинарный образ (jimage), где хранятся модули, включая:

      • java.base (всегда подключён, ядро JDK)

      • java.loggingjava.xmljava.desktop и т.д.

      Bootstrap теперь загружает классы из модульного слоя boot layer, а не напрямую из jar-файлов.


       3. Что он загружает

      • Все классы из модуля java.base — это базовые пакеты:


        java.lang.* java.util.* java.io.* java.nio.* java.math.* java.net.* java.time.*
      • Некоторые внутренние пакеты (jdk.internal.*) — тоже его зона ответственности.

      Пример:


      System.out.println(String.class.getClassLoader()); // null

      null здесь — не ошибка, а признак, что String загружен Bootstrap-загрузчиком (у него нет Java-объекта).


      4. Почему у него нет объекта ClassLoader

      Bootstrap ClassLoader написан на C/C++, встроен в ядро JVM.
      Он не является экземпляром java.lang.ClassLoader и даже не имеет Java-представления.
      Именно поэтому getClassLoader() для базовых классов возвращает null.

      Это сделано специально:

      • для скорости (загрузчик стартует вместе с JVM);

      • для безопасности — нельзя подменить или переопределить его логику.


      5. Почему его нельзя заменить / наследовать

      Причины:

      1. Безопасность: если злоумышленник сможет подменить загрузчик базовых классов (java.lang.Systemjava.security.* и т.д.) — вся платформа компрометируется.

      2. Стабильность: при инициализации JVM уже используются эти классы — Bootstrap должен быть «железно предсказуемым».

      3. Технически невозможно — он живёт в нативном коде JVM, не в Java-куче.


      6. Как Bootstrap работает внутри

      Он встроен в C++-часть JVM, вызывается на старте VM при инициализации модуля java.base.

      Примерно так:


      BootstrapClassLoader → загружает java.base → создаёт базовый модульный слой (boot layer) → инициализирует java.lang.Object, ClassLoader, Thread, System и др. → после этого активируется PlatformClassLoader


       7. Его место в иерархии

      Современная иерархия (Java 9+):


      flowchart TB A[Bootstrap ClassLoader<br>(нативный, загружает java.base)]:::core B[Platform ClassLoader<br>(загружает модули JDK, кроме java.base)]:::core C[Application ClassLoader<br>(загружает классы приложения из classpath)]:::core D[Custom ClassLoader<br>(плагины, шифрованные классы, hot-reload)]:::core A --> B --> C --> D classDef core fill:#f0f7ff,stroke:#4a74b4,stroke-width:1px,color:#0b234a


      8. Что изменилось после Java 9

      Было (до Java 8) Стало (Java 9+)
      Bootstrap грузил классы из rt.jar Теперь грузит из модуля java.base внутри lib/modules
      Extension ClassLoader искал jre/lib/ext Заменён на Platform ClassLoader
      rt.jartools.jar Удалены, заменены модульной структурой
      Можно было «подсунуть» JAR в bootstrap-путь через -Xbootclasspath Теперь для этого есть --patch-module--upgrade-module-path
      Все public-классы JDK были видны через classpath Теперь нужны exports / opens для доступа к ним



      9. Пример: демонстрация уровней загрузчиков


      public class LoaderDemo { public static void main(String[] args) { System.out.println(String.class.getClassLoader()); // null System.out.println(java.sql.Connection.class.getClassLoader()); // Platform System.out.println(LoaderDemo.class.getClassLoader()); // Application } }

      Вывод (в Java 17+):


      null PlatformClassLoader@4e25154f AppClassLoader@18b4aac2


       10. Для продвинутых: как JVM делегирует загрузку


      смотреть диаграмму




       11. Где увидеть «вживую»

      Запустите с флагом:


      java --list-modules

      Покажет все модули, загружаемые Bootstrap/Platform.

      Посмотреть путь загрузки:


      java -Xlog:class+load=info MyApp

      или старый вариант:


      java -verbose:class MyApp


      Ключевой вывод

      Bootstrap ClassLoader не устарел, а:

      • остался фундаментом загрузочной системы JVM;

      • просто перешёл с JAR на модульный формат (jimage);

      • работает полностью на нативном уровне;

      • отвечает за безопасность и консистентность платформы.

  • https://webprogr.ru/~kiFDB
    Короткая ссылка на новость:https://webprogr.ru/~kiFDB


    Последние новости

    ClassLoader’ы: делегирование

    ClassLoader’ы: делегирование

  • Делегирование «снизу вверх»: сначала родитель, потом текущий лоадер.

  • В Java 9+ Extension ClassLoader заменён на Platform ClassLoader.

  • String.class.getClassLoader() возвращает null (загружен Bootstrap’ом) — это не ошибка.


    1) Что делает ClassLoader — коротко и правильно

    ClassLoader — это часть JVM, которая по запросу находит байткод класса (обычно в .class/JAR, а в Java 9+ ещё и в модульном jimage), валидирует его, связывает с остальной программой и при необходимости запускает инициализацию класса (статические блоки/поля).

    Фазы загрузки (упрощённо и в корректной терминологии JVM):

    1. Loading — поиск и чтение байткода, создание объекта java.lang.Class.

    2. Linking — «подружить» класс с VM:

      • Verification — проверка корректности/безопасности байткода.

      • Preparation — выделение памяти под static-поля и их значения по умолчанию.

      • (Optional) Resolution — преобразование символьных ссылок на поля/методы/классы в прямые ссылки (часто лениво, при первом обращении).

    3. Initialization — выполнение статических инициализаторов и присвоение начальных значений static-полям (не «по умолчанию», а именно заданных в коде).

    Class.forName(name) по умолчанию загружает и инициализирует класс.
    Class.forName(name, false, loader) или loader.loadClass(name) — загружают (и линкуют), но не инициализируют до первого «активационного» использования.


    2) Встроенные загрузчики в Java 9+ (JPMS)

    С приходом JPMS (модульной системы) важны три уровня:

    • Bootstrap Class Loader
      Встроен в JVM (нативный), грузит основные модули JDK (в т.ч. java.base) из образа lib/modules (формат jimage). Объекта ClassLoader у него нет — потому String.class.getClassLoader() даёт null.

    • Platform Class Loader (заменил Extension с Java 9)
      Загружает «платформенные» модули JDK/поставщика (то, что не в java.base). Получить: ClassLoader.getPlatformClassLoader().

    • Application (System) Class Loader
      Грузит классы/модули приложения из --class-path или --module-path. Доступ: ClassLoader.getSystemClassLoader().

    Extension ClassLoader (каталог jre/lib/ext) — исторический артефакт до Java 8. В Java 9+ его роль занял Platform ClassLoader.

    Делегирование

    По умолчанию — parent-first: при попытке загрузить класс сначала спрашиваем родителя, и только если тот «не знает», загружаем сами. Это не только ускоряет работу за счёт кешей на верхних уровнях, но и повышает безопасность/предсказуемость (ядро платформы всегда одно и то же).


    3) ClassPath, ModulePath и слои модулей

    • Классическая схема: --class-path (или CLASSPATH) → классы/JAR’ы.

    • Модульная схема (Java 9+): --module-path + module-info.java.

    • Приложение всегда стартует в boot layer модульной системы; дополнительно можно создавать динамические слои (ModuleLayer) для изолированных плагинов.


    4) Контекстный загрузчик (TCCL)

    Многие фреймворки/библиотеки используют Thread Context ClassLoader:




    ClassLoader tccl = Thread.currentThread().getContextClassLoader();

    Это позволяет библиотеке, работающей под своим загрузчиком, находить классы вашего приложения (например, при SPI, ServiceLoader, загрузке обработчиков и т.п.).


    5) Пользовательские ClassLoader’ы: когда и как

    Зачем:

    • Плагин-системы (изолировать зависимости плагинов).

    • «Горячая» перезагрузка классов/многоверсионная загрузка.

    • Загрузка из нестандартных источников (БД, сеть, шифрованные контейнеры).

    • Песочницы (sandbox), частичный контроль доступа (исторически — вместе с Security Manager; ныне он депрекейтед к удалению, потому «песочность» достигается в основном изоляцией слоёв/процессов).

    Минимальный скелет (parent-first, корректно переопределяем findClass):




    public class BytesClassLoader extends ClassLoader { public BytesClassLoader(ClassLoader parent) { super(parent); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] bytes = fetchBytesFor(name); // ваш источник: файл/сеть/шифрованный архив if (bytes == null) throw new ClassNotFoundException(name); return defineClass(name, bytes, 0, bytes.length); } }

    Child-first (редко, но бывает нужно) — специально нарушаем делегирование для «затенения» версии класса из родителя (плагины/OSGi-подобные кейсы). Делать осторожно, чтобы не поймать LinkageError из-за смешения разных Class с одинаковыми именами.


    6) Диагностика проблем загрузки

    Частые исключения/ошибки:

    • ClassNotFoundException — класс не найден загрузчиком (часто: неправильный путь/модуль).

    • NoClassDefFoundError — класс был виден при компиляции, но не найден при выполнении (разошлись classpath/modulepath, «теневой» класс, удалённый JAR).

    • UnsupportedClassVersionError — версия байткода новее, чем поддерживает JVM.

    • IncompatibleClassChangeError / NoSuchMethodError / NoSuchFieldError — бинарная несовместимость (подменили JAR другой версией).

    • LinkageError — общая надкласс-ошибка при конфликте определений класса.

    Как дебажить:

    • Для модулей: запускайте с --show-module-resolution, смотрите, что реально подхватилось.

    • Для classpath: -verbose:class покажет, откуда каждый класс загружен.

    • Явно печатайте cl.getResource("pkg/Class.class"), чтобы увидеть, какой ClassLoader и какой URL.


    7) Сервисы и модульность (SPI)

    Современный способ объявить и найти реализации — ServiceLoader.
    В модульном мире это ещё и директивы:




    // module-info.java module com.acme.app { uses com.acme.spi.Reporter; provides com.acme.spi.Reporter with com.acme.impl.ConsoleReporter; }

    В классическом мире сервисы ищутся по файлам META-INF/services/... и часто используют TCCL.


    8) Java-агенты и трансформация байткода

    Через -javaagent:agent.jar можно перехватывать загрузку классов и трансформировать байткод (профилирование, APM, трассировка):




    public class Agent { public static void premain(String args, Instrumentation inst) { inst.addTransformer((loader, name, classBeingRedefined, domain, bytes) -> { // вернуть модифицированный байткод или null return null; }); } }


    9) Практика: «что, где и как» — правила здорового проекта

    • Не мешайте classpath и modulepath: выбрали JPMS — завершите модульность целиком или чётко поймите границу «автомодулей».

    • Не перегружайте AppClassLoader динамическими путями — используйте URLClassLoader/кастомный загрузчик/динамические модульные слои.

    • Не используйте Class.forName(x).newInstance() — он устаревший. Предпочитайте Class.forName(x).getDeclaredConstructor().newInstance().

    • Соблюдайте «один класс — один загрузчик» в пределах взаимодействующей области: иначе поймаете ClassCastException между логически тем же самым типом, но загруженным другим ClassLoader.

    • Для GraalVM native-image заранее декларируйте всё, что грузите рефлексией/динамически (reflect-config), т.к. в статическом образе нет полноценной динамики загрузки.


    Маленькие, но важные примеры

    1) Разница Class.forName и loadClass:




    Class<?> a = Class.forName("com.example.A"); // загрузит + инициализирует Class<?> b = Class.forName("com.example.B", false, myLoader); // без инициализации Class<?> c = myLoader.loadClass("com.example.C"); // без инициализации

    2) Child-first для плагинов (осторожно!):




    public class ChildFirstURLClassLoader extends URLClassLoader { public ChildFirstURLClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1) сначала проверим: уже загружен? Class<?> c = findLoadedClass(name); if (c == null) { try { c = findClass(name); } // 2) пытаемся САМИ (child-first) catch (ClassNotFoundException ignore) { c = super.loadClass(name, false); } // 3) иначе родитель } if (resolve) resolveClass(c); return c; } } }

    3) Использование ServiceLoader (модульно):




    ServiceLoader<MySpi> loader = ServiceLoader.load(MySpi.class); for (MySpi impl : loader) { impl.run(); }


    Итого — что изменилось «в последних версиях»

    • Extension ClassLoader → Platform ClassLoader (с Java 9).

    • JPMS/модульность: классы ядра грузятся из образа модулей (jimage), у приложения появились module-path и модульные слои.

    • Сильная инкапсуляция JDK: доступ к внутренностям JDK без --add-opens/--add-exports ограничен (это не про загрузку, но влияет на «рефлексивную магию» при инициализации классов).

    • Security Manager объявлен устаревшим к удалению — не рассчитывайте на старые «песочницы»; делайте изоляцию через загрузчики/модули/процессы.

    • Появились виртуальные потоки (Java 21) — это про планирование задач, но на архитектуру загрузчиков не влияет.

    • В продакшене широко используются агенты/трансформеры и AOT/GraalVM: для последних нужно заранее описывать всё динамически загружаемое

      Bootstrap 

      1. Bootstrap ClassLoader 

      Первый и главный загрузчик классов, встроенный в JVM.

      Но:

      • В старых версиях (до Java 8) он загружал классы из файлов rt.jarresources.jarcharsets.jar и т.д.

      • В Java 9 и новее появилась модульная система (JPMS), и теперь Bootstrap работает не с JAR-файлами, а с модульным образом JDK (lib/modules, внутри — бинарный контейнер .jimage).

      То есть Bootstrap ClassLoader остался, но его источник классов полностью поменялся.


       2. Как теперь это устроено (начиная с Java 9)

      Было (до Java 8):



      <JAVA_HOME>/jre/lib/rt.jar <JAVA_HOME>/jre/lib/resources.jar

      Стало (Java 9+):


      <JAVA_HOME>/lib/modules

      Это специальный бинарный образ (jimage), где хранятся модули, включая:

      • java.base (всегда подключён, ядро JDK)

      • java.loggingjava.xmljava.desktop и т.д.

      Bootstrap теперь загружает классы из модульного слоя boot layer, а не напрямую из jar-файлов.


       3. Что он загружает

      • Все классы из модуля java.base — это базовые пакеты:


        java.lang.* java.util.* java.io.* java.nio.* java.math.* java.net.* java.time.*
      • Некоторые внутренние пакеты (jdk.internal.*) — тоже его зона ответственности.

      Пример:


      System.out.println(String.class.getClassLoader()); // null

      null здесь — не ошибка, а признак, что String загружен Bootstrap-загрузчиком (у него нет Java-объекта).


      4. Почему у него нет объекта ClassLoader

      Bootstrap ClassLoader написан на C/C++, встроен в ядро JVM.
      Он не является экземпляром java.lang.ClassLoader и даже не имеет Java-представления.
      Именно поэтому getClassLoader() для базовых классов возвращает null.

      Это сделано специально:

      • для скорости (загрузчик стартует вместе с JVM);

      • для безопасности — нельзя подменить или переопределить его логику.


      5. Почему его нельзя заменить / наследовать

      Причины:

      1. Безопасность: если злоумышленник сможет подменить загрузчик базовых классов (java.lang.Systemjava.security.* и т.д.) — вся платформа компрометируется.

      2. Стабильность: при инициализации JVM уже используются эти классы — Bootstrap должен быть «железно предсказуемым».

      3. Технически невозможно — он живёт в нативном коде JVM, не в Java-куче.


      6. Как Bootstrap работает внутри

      Он встроен в C++-часть JVM, вызывается на старте VM при инициализации модуля java.base.

      Примерно так:


      BootstrapClassLoader → загружает java.base → создаёт базовый модульный слой (boot layer) → инициализирует java.lang.Object, ClassLoader, Thread, System и др. → после этого активируется PlatformClassLoader


       7. Его место в иерархии

      Современная иерархия (Java 9+):


      flowchart TB A[Bootstrap ClassLoader<br>(нативный, загружает java.base)]:::core B[Platform ClassLoader<br>(загружает модули JDK, кроме java.base)]:::core C[Application ClassLoader<br>(загружает классы приложения из classpath)]:::core D[Custom ClassLoader<br>(плагины, шифрованные классы, hot-reload)]:::core A --> B --> C --> D classDef core fill:#f0f7ff,stroke:#4a74b4,stroke-width:1px,color:#0b234a


      8. Что изменилось после Java 9

      Было (до Java 8) Стало (Java 9+)
      Bootstrap грузил классы из rt.jar Теперь грузит из модуля java.base внутри lib/modules
      Extension ClassLoader искал jre/lib/ext Заменён на Platform ClassLoader
      rt.jartools.jar Удалены, заменены модульной структурой
      Можно было «подсунуть» JAR в bootstrap-путь через -Xbootclasspath Теперь для этого есть --patch-module--upgrade-module-path
      Все public-классы JDK были видны через classpath Теперь нужны exports / opens для доступа к ним



      9. Пример: демонстрация уровней загрузчиков


      public class LoaderDemo { public static void main(String[] args) { System.out.println(String.class.getClassLoader()); // null System.out.println(java.sql.Connection.class.getClassLoader()); // Platform System.out.println(LoaderDemo.class.getClassLoader()); // Application } }

      Вывод (в Java 17+):


      null PlatformClassLoader@4e25154f AppClassLoader@18b4aac2


       10. Для продвинутых: как JVM делегирует загрузку


      смотреть диаграмму




       11. Где увидеть «вживую»

      Запустите с флагом:


      java --list-modules

      Покажет все модули, загружаемые Bootstrap/Platform.

      Посмотреть путь загрузки:


      java -Xlog:class+load=info MyApp

      или старый вариант:


      java -verbose:class MyApp


      Ключевой вывод

      Bootstrap ClassLoader не устарел, а:

      • остался фундаментом загрузочной системы JVM;

      • просто перешёл с JAR на модульный формат (jimage);

      • работает полностью на нативном уровне;

      • отвечает за безопасность и консистентность платформы.

  • Рейтинг@Mail.ru