Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture Component

Материалы

ТипСсылка
Документссылка
Видеоссылка

ClassLoader — механизм JVM, отвечающий за поиск и загрузку классов в память. Понимание работы загрузчиков классов критично для диагностики ClassNotFoundException, NoClassDefFoundError, создания плагинных систем и изоляции кода.

Жизненный цикл класса в JVM

Прежде чем класс можно использовать, JVM выполняет три этапа:

┌─────────────┐    ┌─────────────┐    ┌────────────────┐
│   Loading   │───>│   Linking   │───>│ Initialization │
│  (Загрузка) │    │(Связывание) │    │(Инициализация) │
└─────────────┘    └─────────────┘    └────────────────┘
                         │
           ┌─────────────┼─────────────┐
           ▼             ▼             ▼
    ┌────────────┐ ┌────────────┐ ┌────────────┐
    │Verification│ │Preparation │ │ Resolution │
    │ (Проверка) │ │(Подготовка)│ │(Разрешение)│
    └────────────┘ └────────────┘ └────────────┘

Loading (Загрузка)

Загрузчик находит байт-код класса (обычно .class файл) и создаёт объект Class<?> в памяти JVM:

  1. Находит бинарное представление класса по имени
  2. Создаёт объект Class в method area
  3. Записывает связь между классом и загрузчиком

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 ─► НАЙДЕН!

Зачем нужна делегация

  1. Безопасность — нельзя подменить системные классы:
// Даже если создать свой java/lang/String.class в classpath,
// он не загрузится — bootstrap загрузит настоящий String первым
  1. Уникальность — один класс загружается один раз:
// Класс String всегда один и тот же во всём приложении
String.class == String.class  // true, независимо от контекста
  1. Видимость — дочерние загрузчики видят классы родительских:
// 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

Резюме

ЗагрузчикИмяЧто загружает
Bootstrapnulljava.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)