Загрузка классов в Java
Как JVM находит и загружает классы с помощью загрузчиков: bootstrap, platform, system и пользовательских загрузчиков.
Прежде чем JVM выполнит хотя бы одну строку вашего кода, она должна найти файл .class, прочитать его байт-код, верифицировать его и превратить в живой объект Class в памяти. Эта работа возлагается на загрузчик классов. Загрузка классов — именно то, что делает java.lang.String доступным без каких-либо ваших действий, позволяет JAR-файлу из classpath появиться во время выполнения и обеспечивает работу систем плагинов, серверов приложений и инструментов горячей перезагрузки. В этой главе показано, как устроены загрузчики, как работает делегирование и почему идентичность класса — это нечто большее, чем просто его имя.
Иерархия загрузчиков классов
Загрузчики выстраиваются в цепочку родителей, каждый из которых отвечает за свой источник классов. В современном JDK (9+) есть три встроенных загрузчика:
| Загрузчик | Загружает | Отображается как |
|---|---|---|
| Bootstrap | Основные классы JDK (java.*, базовые модули javax.*) | null |
| Platform | Остальные модули платформы JDK | PlatformClassLoader |
| System / Application | Ваш код из classpath/module path | AppClassLoader |
Каждый класс запоминает загрузчик, который его определил. Вы можете узнать у любого класса, каким загрузчиком он был создан:
ClassLoader appLoader = MyApp.class.getClassLoader(); // AppClassLoader
ClassLoader strLoader = String.class.getClassLoader(); // null = bootstrap
ClassLoader parent = appLoader.getParent(); // PlatformClassLoaderBootstrap-загрузчик написан на нативном коде, а не на Java, поэтому String.class.getClassLoader() возвращает null, а не объект — возвращать просто нечего, поскольку Java-экземпляра ClassLoader не существует.
Модель делегирования
Загрузчики классов следуют модели делегирования родителю в первую очередь (parent-first delegation). Получив запрос на загрузку класса, загрузчик не пытается найти его немедленно. Сначала он обращается к родителю, тот — к своему родителю, и так далее вплоть до bootstrap. Только если ни один из предков не может предоставить класс, исходный загрузчик сам пытается его определить.
// Conceptual shape of ClassLoader.loadClass:
protected Class<?> loadClass(String name, boolean resolve) {
Class<?> c = findLoadedClass(name); // already loaded? reuse it
if (c == null) {
try {
c = parent.loadClass(name); // delegate UP first
} catch (ClassNotFoundException e) {
c = findClass(name); // only now load it myself
}
}
return c;
}Такое делегирование гарантирует, что базовые типы загружаются один раз — самым высоким загрузчиком, способным их предоставить. Именно поэтому нельзя переопределить java.lang.String, подложив собственный String.class в classpath — bootstrap-загрузчик первым захватит это имя.
Загрузка, компоновка, инициализация
Жизнь класса начинается в три этапа, и они не одно и то же:
- Загрузка (Loading) — чтение байт-кода и создание объекта
Class. - Компоновка (Linking) — верификация корректности байт-кода, подготовка статических полей со значениями по умолчанию и разрешение символических ссылок.
- Инициализация (Initialization) — выполнение статических инициализаторов и присвоений статических полей (метод
<clinit>класса).
Главный практический факт: инициализация ленива и происходит ровно один раз. Класс инициализируется только при первом активном использовании — первый new, первый вызов статического метода или первое чтение не-константного статического поля.
class Config {
static final Map<String, String> SETTINGS = load(); // runs once, on first touch
static Map<String, String> load() {
System.out.println("Config initialized");
return Map.of("env", "prod");
}
}
// "Config initialized" prints only when Config is first actively used.Пользовательские загрузчики классов
Вы можете расширить ClassLoader, чтобы загружать классы откуда угодно — из базы данных, сетевого потока, сгенерированного байт-кода или зашифрованного JAR-файла. Ключевые методы здесь — findClass (найти и получить байты) и defineClass (передать сырые байты JVM, которая вернёт Class).
class BytesLoader extends ClassLoader {
private final byte[] bytecode;
BytesLoader(byte[] bytecode) { this.bytecode = bytecode; }
@Override
protected Class<?> findClass(String name) {
return defineClass(name, bytecode, 0, bytecode.length);
}
}URLClassLoader — встроенная реализация этой идеи: укажите на JAR-файлы или директории, и он будет загружать классы по мере необходимости:
URL jar = Path.of("plugin.jar").toUri().toURL();
try (URLClassLoader loader = new URLClassLoader(new URL[]{ jar })) {
Class<?> plugin = loader.loadClass("com.example.Plugin");
Object instance = plugin.getDeclaredConstructor().newInstance();
}Идентичность класса: имя плюс загрузчик
Вот тонкость, которая часто вводит людей в заблуждение: идентичность класса во время выполнения — это его полное имя и загрузчик, который его определил. Загрузите байты для Widget через два разных загрузчика, и вы получите два различных объекта Class — не равных между собой, не совместимых по присваиванию — несмотря на то что оба появились из идентичного байт-кода. Именно так серверы приложений изолируют два развёрнутых приложения, которые оба содержат класс с именем com.acme.Util.
Пример в действии: загрузчики, делегирование, ленивость и идентичность
Эта программа не требует внешних классов — она использует загрузчики, уже присутствующие в любой JVM. Она проходит по цепочке загрузчиков, доказывает, что основные классы приходят из bootstrap-загрузчика, демонстрирует делегирование, возвращающее тот же объект Class, наблюдает за ленивым срабатыванием статического инициализатора и определяет один и тот же вручную собранный байт-код через два загрузчика, доказывая правило идентичности «имя плюс загрузчик».
Что можно извлечь из запуска:
- Выведенная цепочка загрузчиков — это живая иерархия загрузчиков классов, где ваш код находится внизу:
ClassLoadingDemoбыл определён загрузчиком уровня приложения, чейgetParent()— следующий загрузчик выше. Каждый загрузчик знает только своего родителя, и цепочка всегда восходит к bootstrap. String.class.getClassLoader()печатаетnull— так JVM сообщает, что класс загружен bootstrap-загрузчиком. Основные типы JDK всегда возвращают здесьnull; объект означал бы, что они пришли от более низкого загрузчика, чего никогда не бывает.app.loadClass("java.lang.StringBuilder") == StringBuilder.classравноtrue. Делегирование передало запрос вверх загрузчику, который уже владеетStringBuilder, поэтому вы получили тот же самый объектClass, а не дубликат — доказательство того, что делегирование предотвращает двойную загрузку базовых типов.Lazy <clinit> runningпечатается один раз — между маркером--- referencing Lazy now ---и первымLazy.VALUE = 42— и больше никогда при втором чтении. Инициализация ленива (она ждала до первого использования) и идемпотентна (статический блок выполняется ровно один раз для одного загрузчика).aиbоба называютсяWidget, ноa == bравноfalse, иa.isAssignableFrom(b)равноfalse. Два загрузчика определили одинаковый байт-код как два различных типа — конкретное доказательство того, что идентичность класса во время выполнения — это полное имя плюс определяющий загрузчик, механизм, лежащий в основе изоляции classpath на серверах приложений.
Практика
Связанные темы
Загрузка классов находится на границе между JVM и вашим кодом, поэтому она затрагивает несколько соседних тем:
- Архитектура JVM — где подсистема загрузчика классов находится среди исполняющего движка и областей данных времени выполнения.
- Модель памяти Java — как загруженные классы и их статические данные хранятся в памяти.
- Сборка мусора — загрузчики классов (и их классы) могут быть выгружены, когда на них больше нет ссылок.
- Введение в модули — на module path загрузка управляется читаемостью модулей, а не плоским classpath.
- Введение в рефлексию —
Class.forNameиloadClass— точки входа, на которых строится рефлексия.