Как на самом деле работает Java ClassLoader system? (с картинками) — Часть 1/3, Загрузка

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Когда я впервые погрузился в мир загрузчиков классов Java, это было ответом на любопытный вопрос. Популярные источники (Wikipedia, Baeldung, DZone) содержат устаревшую, иногда противоречивую друг другу информацию, и это несоответствие послужило толчком для написания этой статьи — поиска ясности в лабиринте ClassLoader System.

Будучи разработчиком Java, вы наверняка сталкивались с ClassNotFoundException или NoClassDefFoundError — загадочными сообщениями, которые на мгновение останавливают наш процесс разработки. Класс не найден — понятно по названию, но не найден где? Кто и как его ищет, куда доставляет?

Попробуем погрузиться в эту тему вместе, отбросив сложности, в стиле небольших диаграмм. Полная картина того, о чем пойдет речь в этой серии статей:

Система загрузчиков классов Java
Система загрузчиков классов Java

Предложение

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

Не существует "универсальной" конструкции виртуальной машины Java.

Спецификация JVM от компании Oracle, устанавливает ожидаемые компоненты и поведение для любой JVM. Однако, эта спецификация не предписывает конкретный подход к реализации этих компонентов, что приводит к тому, что на практике существует целый ряд уникальных реализаций, включая, но не ограничиваясь HotSpot/OpenJDK, Eclipse OpenJ9, GraalVM (основанной на OpenJDK). Каждая из реализаций следует спецификации, но при этом может отличаться по ряду аспектов, как производительность, стратегии сборки мусора и, как несложно предположить, детали процесса загрузки классов.

Отдельный момент, требующий внимания:

Виртуальные машины Java платформо-зависимы.

JVM для Windows OS не идентична JVM для Linux. "Но подождите", — скажете вы, — "я думал, что Java — это все о том, чтобы написать один раз, выполнить везде — независимость от платформы!". Совершенно верно. Однако независимость Java от платформы не означает, что JVM также независима от платформы. Совсем наоборот.

В большинстве статей на эту тему при описании не указывается ни конкретная версия Java, ни описанная реализация VM, что приводит к недопониманию, поскольку JVM развивается и изменяется с каждой версией. Сейчас лето 2023 года, и мир Java находится в предвкушении 21-й версии, но пока она не вышла, мы будем ориентироваться на Java 20, опираясь на саму спецификацию JVM от Oracle, и документацию Oracle Java SE для удобства.

Учитывая это, вернемся к нашей системе загрузчиков


Начиная с основ

Говоря упрощенно, при запуске приложения JVM загружает в память необходимые классы, проверяет байткод, выделяет необходимые ресурсы и, наконец, выполняет код, преобразуя байткод в инструкции машинного языка, понятные конечной машине.

Упрощенный путь преобразования исходного кода Java в нативный код платформы
Упрощенный путь преобразования исходного кода Java в нативный код платформы

Но что на самом деле означает это JVM загружает? Спецификация Java SE приводит следующий комментарий:

Loading refers to the process of finding the binary form of a class or interface with a particular name, perhaps by computing it on the fly, but more typically by retrieving a binary representation previously computed from source code by a Java compiler, and constructing, from that binary form, a Class object to represent the class or interface.

Формулируя более простыя языком, когда мы говорим о "загрузке класса", мы имеем в виду:

Процесс поиска соответствующего файла .class на диске, чтения его содержимого и передачи его в среду выполнения JVM, которая представляет собой определенную часть памяти машины, предназначенную для выполнения вашего приложения.

Погружаясь глубже

В действительности, система загрузчиков классов не просто находит классы — она обеспечивает целостность и безопасность Java-приложения, соблюдая правила бинарной структуры и пространства имен среды выполнения Java.

Стоит добавить, что она обеспечивает гибкость загрузки классов из различных источников — не только из локальной файловой системы, но и по сети, из базы данных или даже сгенерированных налету.

В этой статье мы углубимся в процесс загрузки, но полного понимания стоит упомянуть, что этапа всего 3:

Этапы системы загрузки классов Java
Этапы системы загрузки классов Java

Загрузка (Loading) — Начальная фаза

Процесс начинается с того, что загрузчик класса (далее, ClassLoader) получает задание найти определенный класс, что может быть инициировано самой JVM, или вызвано командой в вашем коде. Задача же здесь заключается в том, чтобы взять полное имя класса (например, java.lang.String) и получить соответствующий файл класса (например, String.class) из его местоположения на диске —> в память JVM.

Этап загрузки, начальный из этапов Системы Загрузчиков Классов Java
Этап загрузки, начальный из этапов Системы Загрузчиков Классов Java

Здесь важно понимать, что подсистема Загрузки — это не одиночный акт, а иерархическая эстафета. Каждый ClassLoader, родительский и дочерний, работает совместно, передавая эстафету ответственности до тех пор, пока нужный класс в конце концов не будет загружен.

Основополагающими принципами, определяющими этот скоординированный процесс загрузки классов, являются (полагайся на диаграмму для понимания):

  • Видимость (Visibility): Дочерний ClassLoader может видеть классы, загруженные его родителем, но не наоборот, что обеспечивает инкапсуляцию;

  • Уникальность (Uniqueness): Класс, загруженный родителем, не будет повторно загружен его дочерним классом, что повышает эффективность;

  • Иерархия делегирования (Delegation Hierarchy): Application ClassLoader (дочерний) передает запрос на загрузку класса родителям, загрузчикам Platform и Bootstrap. Если они не могут найти класс, то запрос передается обратно по цепочке, пока класс не будет найден, или не выкинут соответсвующий ClassNotFoundException

Рассмотрим каждый загрузчик подробнее.

Boostrap ClassLoader

Bootstrap ClassLoader
Bootstrap ClassLoader

Старейший представитель семейства, Bootstrap ClassLoader, отвечает за загрузку основных библиотек Java, расположенных в java.base модуле (java.lang, java.util и т.д.), необходимых для старта JVM.

Обратя внимания на диаграмму можно заметить, что другие загрузчики классов написаны на Java (объекты java.lang.ClassLoader), что означает — их также необходимо загрузить в JVM! Эту задачу также выполняет Bootstrap ClassLoader.

Во многих ресурсах Bootstrap ClassLoader описывается как "родитель" остальных загрузчиков классов. В действительность, это означает лишь логическое наследование, а не наследование Java, поскольку Bootstrap загрузчик написан на native коде, и встроен в виртуальную машину.

Убедимся на практике, что никаких Java загрузчиков, выше самих java.lang загрузчиков нет:

jshell> System.out.println(java.lang.ClassLoader.class.getClassLoader());
null

Bootstrap ClassLoader также является единственным загрузчиком, явно описанным в спецификации Oracle. Остальные зовутся "User-defined", и оставляются на рассмотрение конкретных вендоров вирутальных машин.

Platform ClassLoader

На мой взгляд, самый противоречивый.

Platform ClassLoader
Platform ClassLoader

Документация Java SE 20 говорит о нем следующее:

The platform class loader is responsible for loading the platform classes. Platform classes include Java SE platform APIs, their implementation classes, and JDK-specific run-time classes that are defined by the platform class loader or its ancestors. The platform class loader can be used as the parent of a ClassLoader instance.

Но что отличает классы платформы от основных классов, загружаемых Bootstrap загрузчиком? Посмотрим, что он на самом деле загружает:

jshell> ClassLoader.getPlatformClassLoader().getDefinedPackages();
$1 ==> Package[0] { } // empty

Получается, что в пустой Java-программе — абсолютно ничего! Теперь попробуем явно использовать класс из какого-нибудь стандартного пакета:

jshell> java.sql.Connection.class.getClassLoader()
$2 ==> jdk.internal.loader.ClassLoaders$PlatformClassLoader@27fa135a

jshell> ClassLoader.getPlatformClassLoader().getDefinedPackages()
$3 ==> Package[1] { package java.sql }

Получается, проще говоря, Bootstrap загружает основные классы необходимые для запуска JVM, а Platform — публичные типы системных модулей, которые могут понадобиться. Конкретного разделения необходимых/возможных модулей Java SE я не нашел, но задал соответсвующий вопрос на StackOverFlow, ссылка для любознательных :)

В этом контексте также важно отметить, что во многих источниках (Wiki, Baeldung, последнее обновление 2022, 2023 соотсветственно) Platform ClassLoader обзывают Extension ClassLoader, что на деле не совсем так.

Правильнее было бы утверждать, что Platform ClassLoader пришел на смену Extension ClassLoader, который искал в $JAVA_HOME/lib/ext, и использовался в Java 8 и более ранних версиях. Это изменение произошло с появлением Системы Модулей (JEP-261):

The extension class loader is no longer an instance of URLClassLoader but, rather, of an internal class. It no longer loads classes via the extension mechanism, which was removed by JEP 220. It does, however, define selected Java SE and JDK modules, about which more below. In its new role this loader is known as the platform class loader, it is available via the newClassLoader::getPlatformClassLoader method, and it will be required by the Java SE Platform API Specification.

Application ClassLoader

Application (a.k.a. System) ClassLoader
Application (a.k.a. System) ClassLoader

Application ClassLoader, также известный как системный загрузчик классов, пожалуй, самый user-friendly из всех. Именно этот загрузчик подгружает ваши собственные реализации и библиотеки зависимостей, которые вы передали JVM (явно или неявно) при старте приложения в качестве -classpath (-cp) параметра.

public class HabrTeller {

    public static void main(String[] args) {
        // jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7
        System.out.print(HabrTeller.class.getClassLoader());
    }
}

С точки зрения иерархии, Application загрузчик является порождением Platform загрузчика, и в документации о нем говорится следующее:

This is the default delegation parent for new java.lang.ClassLoader instances, and is typically the class loader used to start the application.

ClassLoader.getSystemClassLoader() method is first invoked early in the runtime's startup sequence, at which point it creates the system class loader. This class loader will be the context class loader for the main application thread (for example, the thread that invokes the main method of the main class).

Резюмирая, именно этот загрузчик является родителем основного потока приложения, и будет являться родителем ваших собственных загрузчиков классов, если вы решите реализовать один.


В дополнение к трем рассмотренным, основным загрузчикам, вы можете создавать свои собственные, пользовательские загрузчики классов, непосредственно в своих Java программах, позволяя обеспечить независимость приложений (чему способствует модель делегирования загрузчиков):

Место пользовательского загрузчика классов в иерархии ClassLoader Loading System
Место пользовательского загрузчика классов в иерархии ClassLoader Loading System

В серверах типа Tomcat, этот подход используется для обеспечения независимой работы различных Web-приложений и корпоративных решений, даже если они размещены на одном сервере. Из популярных открытых примеров, мне удалось найти несколько, для дополнительного ознакомления:

  • Tomcat's Catalina WebappLoader

  • Spring Boot's LaunchedURLClassLoader

Почитать подробнее про обоснование создания собственных, и систему загрузчиков Tomcat как таковую, можно почитать здесь.

Статей по созданию собственных загрузчиков классов написано уже немало, и целью этой статьи служит скорее теория, а не практика, но при должном интересе — можем написать обновленную, отдельную версию.


На этом этапе подпроцесс загрузки подходит к концу: результатом является двоичное представление класса или типа интерфейса в JVM. Однако на этом этапе класс еще не готов к использованию, и мы рассмотрим следующий этап — Linking — во второй части этой серии.

Спасибо, что дочитали до конца! Надеюсь, вы подчеркнули что-то интересное. Данный материал не претендует на звание single source of truth, но мы действительно постарались ссылаться на официальную документацию и спецификацию языка, опуская субьективное и неофициальное.

Источник: https://habr.com/ru/articles/748758/


Интересные статьи

Интересные статьи

Привет! На связи KTS и наш привлеченный эксперт по направлению iOS-разработки Александр.Забрав инициативу у коллеги, возвращаемся с новой статьей из серии, в которой делимся своим представлением о DI ...
И была первая подборка забытых жемчужин от российских игровых разработчиков, и увидел Хабр, что это хорошо. И запилил я вторую. Ну а где вторая, там и третья. Так что встречайте! Перед тем как на...
Думаю, не сильно ошибусь, если скажу, что каждый разработчик программного обеспечения рано или поздно сталкивается с задачей взаимодействия приложений расположенных на удаленных узлах локальной или гл...
Одна из основных задач диалоговых систем состоит не только в предоставлении нужной пользователю информации, но и в генерации как можно более человеческих ответов. А распознание эмоций собеседни...
Итак, у вас есть приложение с автоматическими возобновляемыми подписками. Оно прекрасно работает, пользователи безудержно оформляют премиум подписки и пишут хвалебные отзывы. Красота! Всем при...