Learn · concepts
Что такое загрузчик классов Java?
Загрузчик классов находит и загружает классы Java в виртуальную машину Java во время выполнения. Вот как они работают, почему важна иерархия и почему моды зависят от них.
Загрузчик классов — это часть Java runtime, которая находит файл класса и загружает его в JVM. Каждый раз, когда ваш код обращается к типу впервые, JVM обращается к загрузчику классов с запросом: «Найди этот класс для меня». Загрузчик находит байт-код, читает его, и JVM определяет класс в памяти. Этот механизм невидим в обычных Java-программах, но является точкой входа, которую используют фреймворки модов для патчинга Minecraft.
Что на самом деле делает загрузчик классов
Загрузчик классов преобразует имя класса в используемый класс внутри работающей JVM. Вы запрашиваете тип, загрузчик находит соответствующий файл .class (или генерирует его), проверяет и связывает его, и класс готов к использованию.
Работа происходит в три этапа:
- Найти — Загрузчик находит байт-код для указанного класса (из JAR-файла, с диска, из сети или генерирует его на лету).
- Проверить и связать — JVM проверяет, является ли байт-код допустимым, и разрешает ссылки на другие классы.
- Использовать — Класс готов, и ваша программа может создать его экземпляр.
Именно поэтому Java-программы могут динамически загружать код во время выполнения, который никогда не упоминался во время компиляции. Имени класса достаточно, чтобы загрузчик нашел его.
Иерархия загрузчиков классов
Загрузчики классов организованы в цепочку родительских элементов, и большинство из них сначала обращаются к своему родителю. Стандартное Java-приложение имеет три встроенных загрузчика классов:
| Загрузчик | Загружает | Назначение |
|---|---|---|
| Bootstrap | Основные классы Java runtime | Обеспечивает единообразие основных типов, таких как java.lang.Object |
| Platform | Стандартные библиотечные модули вне ядра | Модули и расширения Java |
| Application | Собственные классы вашей программы | Путь класса, указанный вашей программой |
Когда загрузчик приложений запрашивает класс, он обычно сначала делегирует запрос выше. Он запрашивает платформенный загрузчик, который запрашивает загрузчик Bootstrap, который пытается загрузить его. Только если ни один родитель не может загрузить его, дочерний загрузчик пытается это сделать. Это модель делегирования, и она предотвращает загрузку одного и того же класса дважды под разными именами.
Идентификация класса и принцип ее работы
Класс идентифицируется как по его имени, так и по загрузчику, который его определил. Два разных загрузчика могут загрузить класс с именем com.example.MyClass, и JVM рассматривает их как два совершенно разных типа. Это может показаться туманным, но это основа всего интересного, что может делать Java.
Bootstrap Loader: java.lang.Object (the core Object)
Application Loader: java.lang.Object (if somehow re-loaded by app loader)
→ Two different types, same name
→ This breaks your program, which is why delegation prevents it
Пользовательские загрузчики классов
Программы могут определять свои собственные загрузчики классов. Пользовательский загрузчик может читать байт-код из любого места: из JAR-файла, сети, базы данных или байт-кода, который он сгенерировал или преобразовал на лету. Это делается следующим образом:
- Системы плагинов загружают код динамически, не зная о нём во время компиляции.
- Серверы приложений (Tomcat, JBoss) запускают несколько приложений в одной JVM, каждый со своим собственным загрузчиком классов.
- Моды для Minecraft изменяют классы игры во время их загрузки.
Почему это важно для модов Minecraft
Моды существуют благодаря пользовательской загрузке классов. Загрузчик модов для Minecraft запускает игру через собственный пользовательский загрузчик классов, чтобы он мог видеть и изменять классы игры во время их загрузки. Именно в этой точке перехвата и происходит магия.
Когда загрузчик находится между диском и JVM, он может изменить класс непосредственно перед тем, как JVM определяет его:
Disk: obfuscated Minecraft class
↓
Mod Loader (custom classloader)
↓ [bytecode transformation happens here]
↓
JVM: defines the patched class
↓
Memory: game runs with mods active
Мод может переписать метод игры, внедрить новое поведение, добавить поля, не затрагивая при этом игровой JAR-файл на диске. Класс, который в конечном итоге запускается JVM, — это тот, который был изменен фреймворком мода. Именно так Fabric, Forge и другие загрузчики модов изменяют Minecraft.
Ленивая загрузка
Классы не загружаются все сразу при запуске. JVM загружает каждый класс в первый раз, когда он фактически используется. Это называется ленивой загрузкой:
Program starts → JVM doesn't load String class yet
User calls System.out.println() → String is needed → Loader loads it
Это означает, что загрузчику классов нужно только найти и загрузить классы, к которым обращается ваш код. Неиспользуемые классы никогда не попадают в память. Именно поэтому запуск Minecraft может занять некоторое время (моды добавляют много классов), но не зависает навсегда.
Часто задаваемые вопросы
При первом использовании, а не при запуске. В первый раз, когда ваш код обращается к типу, JVM запрашивает у загрузчика классов найти и определить его. Это называется ленивой загрузкой, и именно поэтому неиспользуемый код никогда не попадает в память.
Загрузчик классов запрашивает у своего родителя загрузку класса, прежде чем пытаться сделать это самостоятельно. Цепочка родительских загрузчиков тянется до загрузчика инициализации. Это предотвращает повторную загрузку одного и того же класса под одним и тем же именем разными загрузчиками.
Да, если они определены разными загрузчиками классов. JVM идентифицирует класс как по его имени, так и по загрузчику, который его определил, поэтому классы с одинаковым именем, но из разных загрузчиков, рассматриваются как отдельные типы.
Перехватывать классы во время их загрузки и изменять их в памяти. Пользовательский загрузчик располагается между диском и JVM, изменяя байткод до того, как JVM определит класс. Именно так моды изменяют Minecraft, не модифицируя игровой jar-файл.