Делегирование «снизу вверх»: сначала родитель, потом текущий лоадер.
В Java 9+ Extension ClassLoader заменён на Platform ClassLoader.
String.class.getClassLoader() возвращает null (загружен Bootstrap’ом) — это не ошибка.
1) Что делает ClassLoader — коротко и правильно
ClassLoader — это часть JVM, которая по запросу находит байткод класса (обычно в .class/JAR, а в Java 9+ ещё и в модульном jimage), валидирует его, связывает с остальной программой и при необходимости запускает инициализацию класса (статические блоки/поля).
Фазы загрузки (упрощённо и в корректной терминологии JVM):
-
Loading — поиск и чтение байткода, создание объекта
java.lang.Class. -
Linking — «подружить» класс с VM:
-
Verification — проверка корректности/безопасности байткода.
-
Preparation — выделение памяти под
static-поля и их значения по умолчанию. -
(Optional) Resolution — преобразование символьных ссылок на поля/методы/классы в прямые ссылки (часто лениво, при первом обращении).
-
-
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.jar,resources.jar,charsets.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.logging,java.xml,java.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()); // nullnullздесь — не ошибка, а признак, чтоStringзагружен Bootstrap-загрузчиком (у него нет Java-объекта).
4. Почему у него нет объекта ClassLoader
Bootstrap ClassLoader написан на C/C++, встроен в ядро JVM.
Он не является экземпляромjava.lang.ClassLoaderи даже не имеет Java-представления.
Именно поэтомуgetClassLoader()для базовых классов возвращаетnull.Это сделано специально:
-
для скорости (загрузчик стартует вместе с JVM);
-
для безопасности — нельзя подменить или переопределить его логику.
5. Почему его нельзя заменить / наследовать
Причины:
-
Безопасность: если злоумышленник сможет подменить загрузчик базовых классов (
java.lang.System,java.security.*и т.д.) — вся платформа компрометируется. -
Стабильность: при инициализации JVM уже используются эти классы — Bootstrap должен быть «железно предсказуемым».
-
Технически невозможно — он живёт в нативном коде 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/modulesExtension ClassLoader искал jre/lib/extЗаменён на Platform ClassLoader rt.jar,tools.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); -
работает полностью на нативном уровне;
-
отвечает за безопасность и консистентность платформы.
-

