Комментарии (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 нет встроенного способа получить все загруженные классы
}
}
Выводы
- Иерархия ClassLoader'ов: Bootstrap → Extension → Application → Custom
- Три этапа: Loading (загрузка файла) → Linking (проверка и подготовка) → Initialization (выполнение static блоков)
- Ленивая загрузка: классы загружаются только когда используются
- Родительская делегация: сначала спрашиваем родителя, потом сами
- Одинаковость классов зависит от загрузчика: один класс, загруженный двумя загрузчиками — это разные классы
- Важно для: плагинов, модулей, контейнеризации, framework'ов