← Назад к вопросам

Как Java загружает классы

2.3 Middle🔥 201 комментариев
#JVM и управление памятью

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Классический вопрос: как Java загружает классы

Это фундаментальный вопрос о Java Runtime. Загрузка классов — это сложный процесс, который начинается ещё до выполнения первой строки кода.

1. Java Class Loading Model

Все начинается с JVM запуска:

public static void main(String[] args) {
    // Ещё ДО этого момента JVM уже загрузила:
    // - java.lang.Object
    // - java.lang.String
    // - java.lang.System
    // - и класс, содержащий main()
}

Процесс загрузки класса имеет несколько этапов:

2. Три класса классозагрузчиков (ClassLoader'ов)

Java использует иерархическую структуру:

Bootstrap ClassLoader (JDK классы: java.lang, java.util)
        ↓
Extension ClassLoader (классы из расширений)
        ↓
Application ClassLoader (классы из CLASSPATH)
        ↓
Custom ClassLoader (твои собственные загрузчики)

Bootstrap ClassLoader

// Загружает ядро JDK (в файле rt.jar или modules)
// Классы: java.lang.Object, String, Integer, ...

Class<?> objectClass = Object.class;
ClassLoader loader = objectClass.getClassLoader();
System.out.println(loader); // null (указывает на Bootstrap)

// Bootstrap не является инстансом ClassLoader

Extension ClassLoader

// Загружает классы из JAVA_HOME/lib/ext
ClassLoader extLoader = ClassLoader.getSystemClassLoader().getParent();
// Редко используется в современной Java

Application ClassLoader

// Загружает классы из CLASSPATH
public class MyApp {
    public static void main(String[] args) {
        Class<?> myClass = MyApp.class;
        ClassLoader loader = myClass.getClassLoader();
        System.out.println(loader); // sun.misc.Launcher$AppClassLoader
    }
}

3. Процесс загрузки класса (Class Loading Phases)

Когда JVM встречает ссылку на класс, происходит загрузка в три этапа:

Этап 1: Loading (Загрузка)

// Когда JVM видит: new UserService()
// ClassLoader ищет класс UserService

// 1. Проверяет Application ClassLoader
UserService.class
    // Ищет класс в CLASSPATH
    // Когда найдёт UserService.class на диске
    // Читает байт-код
    // Создаёт в памяти объект java.lang.Class<?>

// Результат: класс загружен в памяти JVM

Этап 2: Linking (Связывание)

Этап состоит из трёх подэтапов:

2a. Verification (Проверка)

// JVM проверяет, что байт-код легален
// Проверки:
// - Заголовок файла корректен (0xCAFEBABE)
// - Все ссылки между методами/полями существуют
// - Нет несанкционированного доступа (private, protected)
// - Стек корректен (правильное количество аргументов)

public class UserService {
    public User getUser(UUID id) { } // OK
    public void save(User user) { } // OK
    
    // Проверяется, что все используемые классы (UUID, User) существуют
}

2b. Preparation (Подготовка)

// JVM выделяет память для статических полей
public class Counter {
    public static int count = 0; // Выделяется память, инициализируется 0
    public static String name; // Выделяется память, инициализируется null
    
    private int instanceId; // Память выделится при создании инстанса
}

// После Preparation:
// Counter.count = 0
// Counter.name = null

2c. Resolution (Разрешение)

// JVM разрешает символические ссылки
// Заменяет имена классов на реальные ссылки

public class OrderService {
    private UserRepository userRepo; // Символическая ссылка "UserRepository"
    
    public void process(Order order) {
        User user = userRepo.findById(order.getUserId()); // "найти класс UserRepository в памяти"
    }
}

// На этапе Resolution JVM убеждается, что UserRepository уже загружен
// и может его использовать

Этап 3: Initialization (Инициализация)

public class Configuration {
    // Статический блок
    static {
        System.out.println("Configuration инициализируется");
    }
    
    // Статические переменные
    public static final String VERSION = "1.0";
    public static List<String> configs = new ArrayList<>();
    
    static {
        configs.add("debug=true");
        configs.add("timeout=5000");
    }
}

// Когда класс впервые используется (сейчас!)
System.out.println(Configuration.VERSION);
// Output:
// Configuration инициализируется
// 1.0

4. Ленивая загрузка (Lazy Loading)

Классы загружаются только когда они нужны:

public class ApplicationStartup {
    public static void main(String[] args) {
        // В этот момент загружены только:
        // - ApplicationStartup
        // - System, String, Object
        
        System.out.println("Hello"); // java.io.PrintStream уже был загружен
        
        // UserService ещё НЕ загружен!
    }
    
    public void createUser() {
        // Вот СЕЙЧАС загружается UserService
        UserService service = new UserService();
    }
}

// Это оптимизация: зачем загружать класс, если он не нужен?

5. Контекст загрузчика (ClassLoader Context)

public class ServiceFactory {
    
    public static <T> T createService(Class<T> serviceClass) throws Exception {
        // Текущий загрузчик
        ClassLoader currentLoader = Thread.currentThread().getContextClassLoader();
        
        // Загрузить класс используя текущий загрузчик
        Class<?> implClass = currentLoader.loadClass("com.example." + serviceClass.getSimpleName() + "Impl");
        
        return (T) implClass.getDeclaredConstructor().newInstance();
    }
}

// Использование
UserService service = ServiceFactory.createService(UserService.class);
// Сработает, если UserServiceImpl есть в CLASSPATH

6. Проблема: ClassNotFoundException vs NoClassDefFoundError

// ClassNotFoundException — класс не найден при динамической загрузке
try {
    Class<?> cls = Class.forName("com.example.NonExistentClass");
    // Если класса нет, выбросится ClassNotFoundException
} catch (ClassNotFoundException e) {
    System.out.println("Класс не найден");
}

// NoClassDefFoundError — класс был найден при компиляции, но не найден при запуске
public class Main {
    public static void main(String[] args) {
        UserService service = new UserService(); // Компилятор знает о UserService
    }
}
// Если при запуске UserService.class удалён — NoClassDefFoundError!

7. Двойная загрузка и одинаковость классов

// Два ClassLoader'а загружают один и тот же класс
public class MyClass { }

ClassLoader loader1 = new URLClassLoader(new URL[]{...});
ClassLoader loader2 = new URLClassLoader(new URL[]{...});

Class<?> class1 = loader1.loadClass("com.example.MyClass");
Class<?> class2 = loader2.loadClass("com.example.MyClass");

// class1 и class2 — ЭТО РАЗНЫЕ ОБЪЕКТЫ!
System.out.println(class1 == class2); // false

// Даже если это один и тот же исходный класс
// Загруженный разными загрузчиками — это разные классы
MyClass obj1 = (MyClass) class1.getDeclaredConstructor().newInstance();
// MyClass obj2 = obj1; // Ошибка компиляции
// Потому что obj1 — это класс из loader1, а MyClass из Application ClassLoader

8. Родительская делегация (Parent Delegation)

// При загрузке класса используется иерархия:

public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 1. Сначала спрашиваем родителя (Application ClassLoader)
        // Вызывает super.loadClass() по умолчанию
        
        // 2. Если родитель не нашёл, ищем сами
        byte[] classBytes = readClassFromDisk(name);
        return defineClass(name, classBytes, 0, classBytes.length);
    }
}

// Преимущество: java.lang.Object всегда один (загружен Bootstrap)
// Не может быть двух Object'ов из разных загрузчиков

9. Проверка загрузки класса

public class ClassLoadingDemo {
    public static void main(String[] args) {
        // Проверить, загружен ли класс
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        
        try {
            // forName с false не инициализирует
            Class<?> cls = Class.forName("com.example.MyService", false, loader);
            System.out.println("Класс загружен: " + cls.getName());
        } catch (ClassNotFoundException e) {
            System.out.println("Класс не найден");
        }
        
        // Получить все загруженные классы (только через рефлексию)
        // В Java нет встроенного способа получить все загруженные классы
    }
}

Выводы

  1. Иерархия ClassLoader'ов: Bootstrap → Extension → Application → Custom
  2. Три этапа: Loading (загрузка файла) → Linking (проверка и подготовка) → Initialization (выполнение static блоков)
  3. Ленивая загрузка: классы загружаются только когда используются
  4. Родительская делегация: сначала спрашиваем родителя, потом сами
  5. Одинаковость классов зависит от загрузчика: один класс, загруженный двумя загрузчиками — это разные классы
  6. Важно для: плагинов, модулей, контейнеризации, framework'ов
Как Java загружает классы | PrepBro