W3docs

Загрузка классов в Java

Как JVM находит и загружает классы с помощью загрузчиков: bootstrap, platform, system и пользовательских загрузчиков.

Прежде чем JVM выполнит хотя бы одну строку вашего кода, она должна найти файл .class, прочитать его байт-код, верифицировать его и превратить в живой объект Class в памяти. Эта работа возлагается на загрузчик классов. Загрузка классов — именно то, что делает java.lang.String доступным без каких-либо ваших действий, позволяет JAR-файлу из classpath появиться во время выполнения и обеспечивает работу систем плагинов, серверов приложений и инструментов горячей перезагрузки. В этой главе показано, как устроены загрузчики, как работает делегирование и почему идентичность класса — это нечто большее, чем просто его имя.

Иерархия загрузчиков классов

Загрузчики выстраиваются в цепочку родителей, каждый из которых отвечает за свой источник классов. В современном JDK (9+) есть три встроенных загрузчика:

ЗагрузчикЗагружаетОтображается как
BootstrapОсновные классы JDK (java.*, базовые модули javax.*)null
PlatformОстальные модули платформы JDKPlatformClassLoader
System / ApplicationВаш код из classpath/module pathAppClassLoader

Каждый класс запоминает загрузчик, который его определил. Вы можете узнать у любого класса, каким загрузчиком он был создан:

ClassLoader appLoader = MyApp.class.getClassLoader();   // AppClassLoader
ClassLoader strLoader = String.class.getClassLoader();  // null = bootstrap
ClassLoader parent    = appLoader.getParent();          // PlatformClassLoader

Bootstrap-загрузчик написан на нативном коде, а не на 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, наблюдает за ленивым срабатыванием статического инициализатора и определяет один и тот же вручную собранный байт-код через два загрузчика, доказывая правило идентичности «имя плюс загрузчик».

java— editable, runs on the server

Что можно извлечь из запуска:

  • Выведенная цепочка загрузчиков — это живая иерархия загрузчиков классов, где ваш код находится внизу: 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 на серверах приложений.

Практика

Практика
Два отдельных пользовательских загрузчика классов каждый загружает идентичный байт-код для класса с именем 'com.acme.Widget'. Что верно в отношении полученных объектов Class a (от загрузчика 1) и b (от загрузчика 2)?
Два отдельных пользовательских загрузчика классов каждый загружает идентичный байт-код для класса с именем 'com.acme.Widget'. Что верно в отношении полученных объектов Class a (от загрузчика 1) и b (от загрузчика 2)?

Связанные темы

Загрузка классов находится на границе между JVM и вашим кодом, поэтому она затрагивает несколько соседних тем:

  • Архитектура JVM — где подсистема загрузчика классов находится среди исполняющего движка и областей данных времени выполнения.
  • Модель памяти Java — как загруженные классы и их статические данные хранятся в памяти.
  • Сборка мусора — загрузчики классов (и их классы) могут быть выгружены, когда на них больше нет ссылок.
  • Введение в модули — на module path загрузка управляется читаемостью модулей, а не плоским classpath.
  • Введение в рефлексиюClass.forName и loadClass — точки входа, на которых строится рефлексия.
Was this page helpful?