Architecture Component
Материалы
ClassLoader — механизм JVM, отвечающий за поиск и загрузку классов в память. Понимание работы загрузчиков классов критично для диагностики ClassNotFoundException, NoClassDefFoundError, создания плагинных систем и изоляции кода.
Жизненный цикл класса в JVM
Прежде чем класс можно использовать, JVM выполняет три этапа:
┌─────────────┐ ┌─────────────┐ ┌────────────────┐
│ Loading │───>│ Linking │───>│ Initialization │
│ (Загрузка) │ │(Связывание) │ │(Инициализация) │
└─────────────┘ └─────────────┘ └────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│Verification│ │Preparation │ │ Resolution │
│ (Проверка) │ │(Подготовка)│ │(Разрешение)│
└────────────┘ └────────────┘ └────────────┘
Loading (Загрузка)
Загрузчик находит байт-код класса (обычно .class файл) и создаёт объект Class<?> в памяти JVM:
- Находит бинарное представление класса по имени
- Создаёт объект
Classв method area - Записывает связь между классом и загрузчиком
Linking (Связывание)
Verification (Проверка) — JVM проверяет корректность байт-кода:
- Формат файла соответствует спецификации
- Нет нарушений типобезопасности
- Стек операндов не переполняется
Preparation (Подготовка) — выделение памяти для статических полей и установка значений по умолчанию:
class Example {
static int count; // Устанавливается в 0 (не в значение из кода!)
static Object ref; // Устанавливается в null
static final int MAX = 100; // Устанавливается в 100 (ConstantValue)
}
Resolution (Разрешение) — символические ссылки заменяются прямыми. Может происходить лениво (при первом использовании) или сразу.
Initialization (Инициализация)
Выполняется статический инициализатор класса <clinit>:
class Example {
static int count = 42; // Теперь присваивается 42
static List<String> list;
static {
list = new ArrayList<>();
list.add("init");
}
}
Важно: Инициализация происходит только при первом активном использовании класса: создание экземпляра, доступ к статическому полю/методу, рефлексия, инициализация подкласса.
Иерархия встроенных загрузчиков
JVM имеет три встроенных загрузчика классов, образующих иерархию:
┌─────────────────────┐
│ Bootstrap (null) │ ← java.base, java.lang.*
│ Загружает ядро JDK│
└──────────┬──────────┘
│ parent
┌──────────▼──────────┐
│ Platform ("platform")│ ← Java SE API, реализации
│ Платформенные классы │
└──────────┬──────────┘
│ parent
┌──────────▼──────────┐
│ Application ("app") │ ← classpath, module path
│ Классы приложения │
└─────────────────────┘
Bootstrap Class Loader
Загружает базовые классы JDK из модуля java.base: java.lang.*, java.util.*, java.io.* и т.д.
// Bootstrap loader представлен как null
String.class.getClassLoader(); // null
Object.class.getClassLoader(); // null
// Проверка через имя
ClassLoader cl = ArrayList.class.getClassLoader();
System.out.println(cl); // null — загружен bootstrap
Реализован на нативном коде (C/C++), а не на Java.
Platform Class Loader
Загружает платформенные классы Java SE, которые не входят в java.base:
ClassLoader platform = ClassLoader.getPlatformClassLoader();
System.out.println(platform.getName()); // "platform"
// Например, классы из java.sql
java.sql.Connection.class.getClassLoader(); // platform loader
Application (System) Class Loader
Загружает классы приложения из classpath и module path:
ClassLoader app = ClassLoader.getSystemClassLoader();
System.out.println(app.getName()); // "app"
// Ваши классы загружаются этим загрузчиком
MyClass.class.getClassLoader(); // app loader
Classpath определяется:
- Системным свойством
java.class.path - Опцией
-cp/-classpath - Переменной окружения
CLASSPATH - Атрибутом
Class-Pathв MANIFEST.MF
Модель делегирования (Parent Delegation)
Ключевой принцип работы загрузчиков — делегирование родителю:
// Псевдокод алгоритма loadClass
protected Class<?> loadClass(String name, boolean resolve) {
synchronized (getClassLoadingLock(name)) {
// 1. Проверить, не загружен ли класс ранее
Class<?> c = findLoadedClass(name);
if (c == null) {
// 2. Делегировать родителю
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// Родитель не нашёл — это нормально
}
// 3. Если родитель не загрузил — искать самостоятельно
if (c == null) {
c = findClass(name);
}
}
// 4. Опционально: линковка
if (resolve) {
resolveClass(c);
}
return c;
}
}
Порядок поиска класса com.example.MyClass
Application ClassLoader
│
├─► "Уже загружен?" ─► НЕТ
│
├─► Делегирует Platform ClassLoader
│ │
│ ├─► "Уже загружен?" ─► НЕТ
│ │
│ ├─► Делегирует Bootstrap ClassLoader
│ │ │
│ │ └─► "Не найден в java.base"
│ │
│ └─► "Не найден в платформе"
│
└─► Ищет в classpath ─► НАЙДЕН!
Зачем нужна делегация
- Безопасность — нельзя подменить системные классы:
// Даже если создать свой java/lang/String.class в classpath,
// он не загрузится — bootstrap загрузит настоящий String первым
- Уникальность — один класс загружается один раз:
// Класс String всегда один и тот же во всём приложении
String.class == String.class // true, независимо от контекста
- Видимость — дочерние загрузчики видят классы родительских:
// Application loader видит java.util.List (загружен bootstrap)
// Bootstrap loader НЕ видит com.example.MyClass (загружен application)
API класса ClassLoader
Получение загрузчиков
// Загрузчик конкретного класса
ClassLoader cl = MyClass.class.getClassLoader();
// Системный (application) загрузчик
ClassLoader system = ClassLoader.getSystemClassLoader();
// Платформенный загрузчик
ClassLoader platform = ClassLoader.getPlatformClassLoader();
// Родительский загрузчик
ClassLoader parent = cl.getParent();
// Контекстный загрузчик текущего потока
ClassLoader context = Thread.currentThread().getContextClassLoader();
Загрузка классов
// Через Class.forName (инициализирует класс)
Class<?> cls = Class.forName("com.example.MyClass");
// Через Class.forName с контролем инициализации
Class<?> cls = Class.forName("com.example.MyClass", false, classLoader);
// Через ClassLoader.loadClass (НЕ инициализирует)
Class<?> cls = classLoader.loadClass("com.example.MyClass");
Разница:
Class.forName()по умолчанию инициализирует класс (выполняет static-блоки),loadClass()— нет.
Загрузка ресурсов
ClassLoader cl = MyClass.class.getClassLoader();
// Один ресурс
URL url = cl.getResource("config/app.properties");
InputStream is = cl.getResourceAsStream("config/app.properties");
// Все ресурсы с данным именем (из разных JAR)
Enumeration<URL> urls = cl.getResources("META-INF/services/MyService");
// Stream API (Java 9+)
Stream<URL> stream = cl.resources("META-INF/MANIFEST.MF");
Путь к ресурсу:
- Без
/— относительно корня classpath:"config/app.properties" - Ищется через делегацию, как и классы
// Через Class (относительно пакета класса или абсолютно)
MyClass.class.getResource("local.txt"); // В том же пакете
MyClass.class.getResource("/config/global.txt"); // От корня classpath
Создание собственного ClassLoader
Базовая структура
public class CustomClassLoader extends ClassLoader {
private final Path classesDir;
public CustomClassLoader(Path classesDir, ClassLoader parent) {
super(parent);
this.classesDir = classesDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// Преобразуем имя класса в путь к файлу
String fileName = name.replace('.', '/') + ".class";
Path classFile = classesDir.resolve(fileName);
try {
byte[] bytes = Files.readAllBytes(classFile);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("Class not found: " + name, e);
}
}
}
Ключевые методы для переопределения
| Метод | Когда переопределять |
|---|---|
findClass(String) | Основной метод — откуда брать байт-код |
loadClass(String, boolean) | Изменить стратегию делегирования |
findResource(String) | Откуда брать ресурсы |
findLibrary(String) | Где искать native-библиотеки |
Network ClassLoader
Загрузчик, скачивающий классы по сети:
public class NetworkClassLoader extends ClassLoader {
private final URL baseUrl;
public NetworkClassLoader(URL baseUrl, ClassLoader parent) {
super(parent);
this.baseUrl = baseUrl;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = name.replace('.', '/') + ".class";
try {
URL classUrl = new URL(baseUrl, path);
try (InputStream is = classUrl.openStream()) {
byte[] bytes = is.readAllBytes();
return defineClass(name, bytes, 0, bytes.length);
}
} catch (IOException e) {
throw new ClassNotFoundException("Failed to load: " + name, e);
}
}
}
// Использование
URL serverUrl = new URL("https://plugins.example.com/classes/");
ClassLoader loader = new NetworkClassLoader(serverUrl, getClass().getClassLoader());
Class<?> pluginClass = loader.loadClass("com.example.Plugin");
Object plugin = pluginClass.getConstructor().newInstance();
Encrypting ClassLoader
Загрузчик зашифрованных классов:
public class DecryptingClassLoader extends ClassLoader {
private final Path encryptedDir;
private final SecretKey key;
public DecryptingClassLoader(Path dir, SecretKey key, ClassLoader parent) {
super(parent);
this.encryptedDir = dir;
this.key = key;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = name.replace('.', '/') + ".class.enc";
Path encryptedFile = encryptedDir.resolve(fileName);
try {
byte[] encrypted = Files.readAllBytes(encryptedFile);
byte[] decrypted = decrypt(encrypted);
return defineClass(name, decrypted, 0, decrypted.length);
} catch (Exception e) {
throw new ClassNotFoundException("Failed to load: " + name, e);
}
}
private byte[] decrypt(byte[] data) throws Exception {
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(data);
}
}
Child-First (Parent-Last) ClassLoader
Иногда нужно загружать классы до делегации родителю — например, для изоляции версий библиотек:
public class ChildFirstClassLoader extends URLClassLoader {
private final Set<String> childFirstPackages;
public ChildFirstClassLoader(URL[] urls, ClassLoader parent,
Set<String> childFirstPackages) {
super(urls, parent);
this.childFirstPackages = childFirstPackages;
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// Проверяем кэш
Class<?> c = findLoadedClass(name);
if (c == null) {
// Проверяем, нужно ли загружать самим (child-first)
if (shouldLoadFirst(name)) {
try {
c = findClass(name);
} catch (ClassNotFoundException e) {
// Если не нашли — делегируем родителю
}
}
// Стандартная делегация
if (c == null) {
c = super.loadClass(name, false);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
private boolean shouldLoadFirst(String name) {
// Системные классы всегда через родителя
if (name.startsWith("java.") || name.startsWith("javax.") ||
name.startsWith("sun.") || name.startsWith("jdk.")) {
return false;
}
// Проверяем список пакетов для child-first
for (String pkg : childFirstPackages) {
if (name.startsWith(pkg)) {
return true;
}
}
return false;
}
}
Parallel Capable ClassLoaders
В многопоточной среде важно избегать deadlock при загрузке классов. Parallel capable загрузчики используют отдельные блокировки для каждого класса:
public class ParallelClassLoader extends ClassLoader {
// Регистрация в static-блоке — ДО создания экземпляров!
static {
registerAsParallelCapable();
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// getClassLoadingLock возвращает объект блокировки для конкретного класса
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// Реализация загрузки
return super.findClass(name);
}
}
// Проверка
System.out.println(loader.isRegisteredAsParallelCapable()); // true
Важно:
registerAsParallelCapable()должен вызываться в static-блоке до создания любых экземпляров, и все родительские классы (кроме Object) тоже должны быть parallel capable.
URLClassLoader
Стандартный загрузчик для работы с JAR-файлами и директориями:
// Создание с указанием URL
URL[] urls = {
new File("/path/to/classes/").toURI().toURL(),
new File("/path/to/library.jar").toURI().toURL(),
new URL("https://repo.example.com/plugin.jar")
};
URLClassLoader loader = new URLClassLoader(urls, getClass().getClassLoader());
// Загрузка класса
Class<?> cls = loader.loadClass("com.example.Plugin");
// Важно: закрывать после использования (реализует Closeable)
loader.close();
// Или с try-with-resources
try (URLClassLoader loader = new URLClassLoader(urls)) {
Class<?> cls = loader.loadClass("com.example.Plugin");
// ...
}
Динамическое добавление JAR (до Java 9)
// До Java 9 можно было добавлять URL динамически через рефлексию
// В Java 9+ URLClassLoader — read-only после создания
// Альтернатива: создавать новый загрузчик с расширенным списком URL
Context ClassLoader
Контекстный загрузчик потока решает проблему “обратной видимости”, когда код из родительского загрузчика должен загрузить класс из дочернего:
// Проблема: JDBC-драйвер загружен в Application ClassLoader,
// а DriverManager — в Bootstrap ClassLoader
// Решение: контекстный загрузчик
ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
// SPI (ServiceLoader) использует контекстный загрузчик
ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
// Эквивалентно:
ServiceLoader.load(Driver.class, Thread.currentThread().getContextClassLoader());
Установка контекстного загрузчика
ClassLoader myLoader = new CustomClassLoader(...);
Thread.currentThread().setContextClassLoader(myLoader);
try {
// Код, который должен использовать myLoader
ServiceLoader<MyPlugin> plugins = ServiceLoader.load(MyPlugin.class);
} finally {
// Восстановить оригинальный
Thread.currentThread().setContextClassLoader(originalLoader);
}
Проблемы и решения
ClassNotFoundException vs NoClassDefFoundError
// ClassNotFoundException — класс не найден при явном запросе
try {
Class.forName("com.nonexistent.Class");
} catch (ClassNotFoundException e) {
// Checked exception — нужно обрабатывать
}
// NoClassDefFoundError — класс был доступен при компиляции,
// но не найден при выполнении
try {
new SomeClass(); // SomeClass зависит от MissingDependency
} catch (NoClassDefFoundError e) {
// Error — обычно фатальная ошибка
// Часто обёртка над ClassNotFoundException
Throwable cause = e.getCause(); // Может быть ClassNotFoundException
}
Class Identity
Класс уникален парой: бинарное имя + defining class loader:
URLClassLoader loader1 = new URLClassLoader(urls);
URLClassLoader loader2 = new URLClassLoader(urls);
Class<?> cls1 = loader1.loadClass("com.example.MyClass");
Class<?> cls2 = loader2.loadClass("com.example.MyClass");
cls1 == cls2; // false! Разные классы
cls1.equals(cls2); // false!
Object obj1 = cls1.getConstructor().newInstance();
cls2.isInstance(obj1); // false! obj1 — не экземпляр cls2
cls2.cast(obj1); // ClassCastException!
Это позволяет изолировать версии:
┌─────────────────┐ ┌─────────────────┐
│ Plugin A │ │ Plugin B │
│ guava-31.0.jar │ │ guava-28.0.jar │
│ (LoaderA) │ │ (LoaderB) │
└─────────────────┘ └─────────────────┘
Memory Leaks
Классы не выгружаются, пока жив их загрузчик:
// Утечка памяти в сервлет-контейнерах
public class BadServlet extends HttpServlet {
// Статическая ссылка на Thread держит ClassLoader
private static Thread worker = new Thread(() -> {
while (true) { /* работа */ }
});
}
Решение:
// Останавливать потоки при undeploy
@Override
public void destroy() {
worker.interrupt();
worker = null;
}
// Избегать статических ссылок на классы из webapp classloader
// Использовать слабые ссылки (WeakReference) где возможно
Практический пример: Plugin System
public interface Plugin {
String getName();
void execute();
}
public class PluginManager {
private final Map<String, PluginInfo> plugins = new ConcurrentHashMap<>();
private final Path pluginsDir;
public PluginManager(Path pluginsDir) {
this.pluginsDir = pluginsDir;
}
public void loadPlugin(String jarName) throws Exception {
Path jarPath = pluginsDir.resolve(jarName);
URL[] urls = { jarPath.toUri().toURL() };
// Каждый плагин — свой загрузчик (изоляция)
URLClassLoader loader = new URLClassLoader(urls,
Plugin.class.getClassLoader());
// Читаем имя класса из манифеста
try (JarFile jar = new JarFile(jarPath.toFile())) {
String mainClass = jar.getManifest()
.getMainAttributes()
.getValue("Plugin-Class");
Class<?> cls = loader.loadClass(mainClass);
Plugin plugin = (Plugin) cls.getConstructor().newInstance();
plugins.put(plugin.getName(), new PluginInfo(plugin, loader));
System.out.println("Loaded: " + plugin.getName());
}
}
public void unloadPlugin(String name) throws Exception {
PluginInfo info = plugins.remove(name);
if (info != null) {
info.loader().close(); // Закрываем загрузчик
System.out.println("Unloaded: " + name);
}
}
public void executePlugin(String name) {
PluginInfo info = plugins.get(name);
if (info != null) {
info.plugin().execute();
}
}
private record PluginInfo(Plugin plugin, URLClassLoader loader) {}
}
// Использование
PluginManager manager = new PluginManager(Path.of("plugins"));
manager.loadPlugin("my-plugin.jar");
manager.executePlugin("MyPlugin");
manager.unloadPlugin("MyPlugin");
Модульная система и ClassLoader (Java 9+)
В модульной системе загрузчики работают с модулями:
// Unnamed module — классы из classpath
Module unnamed = MyClass.class.getModule();
unnamed.isNamed(); // false
// Named module
Module base = String.class.getModule();
base.getName(); // "java.base"
// Каждый загрузчик имеет свой unnamed module
ClassLoader cl = getClass().getClassLoader();
Module unnamedModule = cl.getUnnamedModule();
Слои (Layers) позволяют организовывать модули:
ModuleLayer bootLayer = ModuleLayer.boot();
// Содержит модули из java.base и т.д.
// Можно создавать пользовательские слои с custom class loaders
Резюме
| Загрузчик | Имя | Что загружает |
|---|---|---|
| Bootstrap | null | java.base, java.lang., java.util. |
| Platform | "platform" | Java SE Platform API |
| Application | "app" | classpath, module path |
Ключевые принципы:
- Делегирование — сначала родитель, потом сам
- Уникальность — класс = имя + defining loader
- Видимость — дочерние видят классы родительских, но не наоборот
Когда создавать свой ClassLoader:
- Загрузка из нестандартных источников (сеть, БД, шифрованные файлы)
- Изоляция плагинов/модулей с конфликтующими зависимостями
- Hot-reload кода без перезапуска JVM
- Байт-код манипуляции (инструментирование, AOP)
Методы для переопределения:
| Цель | Метод |
|---|---|
| Откуда брать байт-код | findClass(String) |
| Изменить делегирование | loadClass(String, boolean) |
| Откуда брать ресурсы | findResource(String) |
| Где искать native-библиотеки | findLibrary(String) |