Загрузка классов в Java. Теория
Одной из основных особенностей платформы Java является модель динамической загрузки классов, которая позволяет загружать исполняемый код в JRE не перезагружая основое приложение. Такая особенность широко используется в серверах приложений, получивших последнее время высокую популярность.
В статье рассмотрены базовые понятия, аспекты и принципы модели динамической загрузки кода. В следующей статье будет рассмотрена реализация собственного загрузчика классов, как основного механизма приложения с плагино-модульной архитектурой.
Введение
Классы загружаются по мере надобности, за небольшим исключением. Некоторые базовые классы из rt.jar (java.lang.* в частности) загружаются при старте приложения. Классы расширений ($JAVA_HOME/lib/ext), пользовательские и большинство системных классов загружаются по мере их использования.
Виды загрузчиков
Различают 3-и вида загрузчиков в Java. Это — базовый загрузчик (bootstrap), системный загрузчик (System Classloader), загрузчик расширений (Extension Classloader).
System Classloader — системный загрузчик, реализованный уже на уровне JRE. В Sun JRE — это класс sun.misc.Launcher$AppClassLoader. Этим загрузчиком загружаются классы, пути к которым указаны в переменной окружения CLASSPATH.
Управлять загрузкой расширений можно с помощью системной опции java.ext.dirs.
Понятия
Различают текущий загрузчик (Current Classloader) и загрузчик контекста (Context Classloader).
Current Classloader — это загрузчик класса, код которого в данный момент исполняется. Текущий загрузчик используется по умолчанию для загрузки классов в процессе исполнения. В часности, при использовании метода Class.forName(«»)/ClassLoader.loadClass(«») или при любой декларации класса, ранее не загруженного.
Context Classloader — загрузчик контекста текущего потока. Получить и установить данный загрузчик можно с помощью методов Thread.getContextClassLoader()/Thread.setContextClassLoader(). Загрузчик контекста устанавливается автоматически для каждого нового потока. При этом, используется загрузчик родительского потока.
Модель делегирования загрузки
Начиная с версии Java 2 Platform, Standard Edition, v1.2 загрузчики классов образуют иерархию. Корневым является базовый (у него предка нет). Все остальные загрузчики при инициализации инстанциируют ссылку на родительский загрузчик. Такая иерархия необходима для модели делегирования загрузки. В общем случа, иерархия выглядит следующим образом.
Право загрузки класса рекурсивно делегируется от самого нижнего загрузчика в иерархии к самому верхнему. Такой подход позволяет загружать классы тем загрузчиком, который максимально близко находится к базовому. Так достигается максимальная область видимости классов. Под областью видимости подразумевается следующее. Каждый загрузчик ведет учет классов, которые были им загружены. Множество этих классов и назвается областью видимости.
Рассмотрим процесс загрузки более детально. Пусть в систем исполнения встретилась декларация переменной пользовательского класс Student.
1) Системный загрузчик попытается поискать в кеше класс Student.
_1.1) Если класс найден, загрузка окончена.
_1.2) Если класс не найден, загрузка делегируется загрузчику расширений.
2) Загрузчик расширений попытается поискать в кеше класс Student.
_2.1) Если класс найден, загрузка окончена.
_2.2) Если класс не найден, загрузка делегируется базовому загрузчику.
3) Базовый загрузчик попытается поискать в кеше класс Student.
_3.1) Если класс найден, загрузка окончена.
_3.2) Если класс не найден, базовый загрузчик попытается его загрузить.
__3.2.1) Если загрузка прошла успешно, она закончена 😉
__3.2.2) Иначе управление предается загрузчику раширений.
_3.3) Загрузчик расширений пытается загрузить класс.
__3.3.1) Если загрузка прошла успешно, она закончена 😉
__3.3.2) Иначе управление предается системному загрузчику.
_3.4) Системный загрузчик пытается загрузить класс.
__3.4.1) Если загрузка прошла успешно, она закончена 😉
__3.4.2) Иначе генерируется исключение java.lang.ClassNotFoundException.
Если в системе присутствуют пользовательские загрузчики, они должны
а) расширять класс java.lang.ClassLoader;
б) поддерживать модель динамической загрузки.
Inside
public class B extends A
public class C extends B
Вывод показывает, что классы были загружены не в том порядке в котором были использованы. Это обусловлено наследованием.
[Loaded Main from file:/C:/devel/CL/bin/]
[Loaded A from file:/C:/devel/CL/bin/]
[Loaded B from file:/C:/devel/CL/bin/]
[Loaded C from file:/C:/devel/CL/bin/]
Внутренности JVM, Часть 1 — Загрузчик классов
Перевод статьи подготовлен специально для студентов курса «Разработчик Java».
В этой серии статей я расскажу о том, как работает Java Virtual Machine. Сегодня мы рассмотрим механизм загрузки классов в JVM.
Виртуальная машина Java — это сердце экосистемы Java-технологий. Она делает для Java-программ возможность реализации принципа «написано один раз, работает везде» (write once run everywhere). Как и другие виртуальные машины, JVM представляет собой абстрактный компьютер. Основная задача JVM — загружать class-файлы и выполнять содержащийся в них байт-код.
В состав JVM входят различные компоненты, такие как загрузчик классов (Classloader), сборщик мусора (Garbage Collector) (автоматическое управление памятью), интерпретатор, JIT-компилятор, компоненты управления потоками. В этой статье рассмотрим загрузчик классов (Class loader).
Загрузчик классов загружает class-файлы как для вашего приложения, так и для Java API. В виртуальную машину загружаются только те class-файлы Java API, которые действительно требуются при выполнении программы.
Байт-код выполняется подсистемой исполнения (execution engine).
Что такое загрузка классов?
Загрузка классов — это поиск и загрузка типов (классов и интерфейсов) динамически во время выполнения программы. Данные о типах находятся в бинарных class-файлах.
Этапы загрузки классов
Подсистема загрузчика классов отвечает не только за поиск и импорт бинарных данных класса. Она также выполняет проверку правильности импортируемых классов, выделяет и инициализирует память для переменных класса, помогает в разрешении символьных ссылок. Эти действия выполняются в следующем порядке:
Примечание — загрузчик классов, помимо загрузки классов, также отвечает за поиск ресурсов. Ресурс — это некоторые данные (например, “.class” файл, данные конфигурации, изображения), которые идентифицируются с помощью абстрактного пути, разделенного символом «/». Ресурсы обычно упаковываются вместе с приложением или библиотекой для того, чтобы их можно было использовать в коде приложения или библиотеки.
Механизм загрузки классов в Java
Platform class loader — загружает выбранные (на основе безопасности / разрешений) модули Java SE и JDK. Например, java.sql.
Bootstrap class loader — загружает основные модули Java SE и JDK.
Эти три встроенных загрузчика классов работают вместе следующим образом:
Запустив этот код на установленном у меня Amazon Corretto 11.0.3, получим следующий результат:
Подробнее изучить ClassLoader API вы можете здесь (JDK 11).
Загрузка классов в Java. Практика
Данная статья является продолжением статьи Загрузка классов в Java. Теория.
В статье рассмотрена реализация каркаса приложения с плагино-модульной архитектурой. В качестве движка приложения будет использоваться пользовательский загрузчик классов, которым будут загружаться дополнительные плагины приложения.
Код приложения не претендует на оригинальность, а лишь объясняет подходы и принципы написания пользовательских загрузчиков классов и методы инвокации динамического кода.
Мотивация
Зачастую, архитектура сложных систем подразумевает использование механизма динамической загрузки кода. Это бывает необходимо, когда заранее не известно какой именно код будет исполняться в рантайме. Например, всем известная игра для программистов Robocode использует собственный загрузчик классов, для загрузки пользовательских танков в игру. Можно рассматривать отдельный танк как модуль, разрабатываемый изолированно от приложения по заданному интерфейсу. Похожая ситуация рассматривается в статье, только на максимально упрощенном уровне.
Кроме того, можно привести еще несколько очевидных примеров использования механизма динамической загрузки кода. Допустим байт-код классов хранится в БД. Очевидно, что для загрузки таких классов нужен специальный загрузчик, в обязанности которого будет входить еще и выборка кода классов из БД.
Возможно, классы требуется загружать по сети/через интернет. Для таких целей нужен загрузчик, способный получать байт-код по одному из сетевых протоколов. Можно также выделить, существующий в Java Class Library URLClassLoader, который способен загружать классы по указанному пути в URL.
Подготовка
Реализуемое в рамках статьи приложение будет представлять собой каркас движка для динамической загрузки кода в JRE и его исполнения. Каждый модуль будет представлять собой один Java класс, реализующий интерфейс Module. Общий для всех модулей интерфейс необходим для их инвокации. Здесь, важно понимать, что существует еще один способ исполнения динамического кода — Java Reflection API. Однако, для большей наглядности и простоты будет использоваться модель с общим интерфейсом.
При реализации пользовательских загрузчиков важно помнить следующее:
1) любой загрузчик должен явно или неявно расширять класс java.lang.ClassLoader;
2) любой загрузчик должен поддерживать модель делегирования загрузки, образуя иерархию;
3) в классе java.lang.ClassLoader уже реализован метод непосредственной загрузки — defineClass(. ), который байт-код преобразует в java.lang.Class, осуществляя его валидацию;
4) механизм рекурентного поиска также реализован в классе java.lang.ClassLoader и заботиться об это не нужно;
5) для корректной реализации загрузчика достаточно лишь переопределить метод findClass() класса java.lang.ClassLoader.
Рассмотрим детально поведение загрузчика классов при вызове метода loadClass() для объяснения последнего пункта вышеуказанного списка.
Реализация по-умолчанию подразумевает следующую последовательность действий:
1) вызов findLoadedClass() для поиска загружаемого класса в кеше;
2) если класса в кеше не оказалось, происходит вызов getParent().loadClass() для делегирования права загрузки родительскому загрузчику;
3) если иерархия родительских загрузчиков не смогла загрузить класс, происходит вызов findClass() для непосредственной загрузки класса.
Поэтому для правильной реализации загрузчиков рекомендуется придерживаться указанного сценария — переопределения метода findClass().
Реализация
Определим интерфейс модулей. Пусть модуль сначала загружается (load), потом исполняется (run), возвращая результат и затем уже выгружается (unload). Данный код представляет собой API для разработки модулей. Его можно скомпилировать отдельно и упаковать в *.jar для поставки отдельно от основного приложения.
public static final int EXIT_SUCCESS = 0;
public static final int EXIT_FAILURE = 1;
public void load();
public int run();
public void unload();
Рассмотрим реализацию загрузчика модулей. Данный загрузчик загружает код классов из определенной директории, путь к которой указан в переменной pathtobin.
import java.io. File ;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class ModuleLoader extends ClassLoader <
/**
* Путь до директории с модулями.
*/
private String pathtobin;
@Override
public Class findClass( String className) throws ClassNotFoundException <
try <
/**
* Получем байт-код из файла и загружаем класс в рантайм
*/
byte b[] = fetchClassFromFS(pathtobin + className + «.class» );
return defineClass(className, b, 0, b.length);
> catch (FileNotFoundException ex) <
return super.findClass(className);
> catch (IOException ex) <
return super.findClass(className);
>
/**
* Взято из www.java-tips.org/java-se-tips/java.io/reading-a-file-into-a-byte-array.html
*/
private byte [] fetchClassFromFS( String path) throws FileNotFoundException, IOException <
InputStream is = new FileInputStream( new File (path));
// Get the size of the file
long length = new File (path).length();
if (length > Integer.MAX_VALUE) <
// File is too large
>
// Create the byte array to hold the data
byte [] bytes = new byte [( int )length];
// Ensure all the bytes have been read in
if (offset throw new IOException( «Could not completely read file » +path);
>
public class ModuleEngine <
public static void main( String args[]) <
String modulePath = args[0];
/**
* Создаем загрузчик модулей.
*/
ModuleLoader loader = new ModuleLoader(modulePath, ClassLoader.getSystemClassLoader());
/**
* Получаем список доступных модулей.
*/
File dir = new File (modulePath);
String [] modules = dir.list();
/**
* Загружаем и исполняем каждый модуль.
*/
for ( String module: modules) <
try <
String moduleName = module.split( «.class» )[0];
Class clazz = loader.loadClass(moduleName);
Module execute = (Module) clazz.newInstance();
execute.load();
execute.run();
execute.unload();
> catch (ClassNotFoundException e) <
e.printStackTrace();
> catch (InstantiationException e) <
e.printStackTrace();
> catch (IllegalAccessException e) <
e.printStackTrace();
>
>
public class ModulePrinter implements Module <
Скомпилировав данный код, результат в виде одного class файла можно скопировать в отдельную директорию, путь к которой необходимо указать в качестве параметра основного приложения.
java — Отражение; Динамическая загрузка классов
Эта статья состоит из 3 — х частей:
1. Описание возможностей отражения
2. Немного о ClassLoader’е
3. Пишем (практика) загрузчик плагинов для разных проектов
Часть 1
Так как java код сначал «превращается» в промежуточный байткод, а только потом
в jre на конкретной машине с помощью JIT(Just In Time)-компиляции превращается в
настоящий код или интерпртируется интерпретатором java, поэтому появилась технология «Отражение» — способность программы изменять
саму себя во время работы.
Давайте подробно рассмотрим эти возможности.
Чтобы не быть голословным я накидал маленький класс.
Особого смысла не ищите в коде, просто наглядное пособие.
Чтобы воздействовать с классом нам нужен (вы не поверите) объект типа Class:
Получить его можно несколькими способами:
1. Если класс заранее известен, то можно воспользоваться конструкцией Color.class (пример)
2. Через экземпляр объекта: AnObject.getClass()
3. Пользуясь объектом ClassLoader или Class.forName() (-> 2 часть)
Class следует записывать как Class
Потому что скобки дают несколько возможностей:
Например указать класс напрямую:
Class ColorClass= Color.class;
или Указать что класс наследник другого
Class ColorClass = Color.class;//(любой класс — подкласс Object)
Теперь давайте рассмотрим возможности Class:
Сначала начнём с простого и создадим экземпляр:
Для этого нам нужно
1. Получить конструктор по его параметрам
2. Вызвать его и присвоить результат новой переменной.
Теперь давайте попробуем вызвать наш публичный метод blend(не статичный) и newInstance (статичный).
——Замечания (1)——
Для некоторых вещей понадобиться импортировать java.lang.reflect.*;
— Теперь давайте сделаем что-нибудь интересное: например вызовем private методы Average и getSecret.
(с конструктором всё также, я не буду повторятся для экономии Вашего времени)
1 Для начала получим два метода (как в прошлый раз).
2 Сами себе разрешим доступ.
3 Вызовем его как обычно.
Не забываем добавить те же проверки на исключения, запускаем. OK.
>2100855933
>8
Теперь давайте расскажу про поля… с ними ситуация похожая:
Class.getDeclaredField(String name) возвращает Field
бросает:
NoSuchFieldException — Нет такого поля
SecurityException — Нет прав
SomeClass.getDeclaredFields() — массив Field[], все поля класса.
Field.setAcessible(true) разрешает доступ.
========ЧАСТЬ 2=========
==Немного о ClassLoader ==
Итак ClassLoader служит для загрузки классов. Когда вы запускаете java-программу
вы сразу загружаете ClassLoader’ом главный класс. Каждый класс «помнит» ClassLoader который
его загрузил (Class.getClassLoader(); ObjectA.getClassLoader();)
Когда ему(экземпляру класса) понадобиться обратиться к другим классам он обратиться к этому ClassLoader’у
и загрузит их. Поэтому если вы не касаетесь этой области, то все классы будет загружать системный ClassLoader.
Также если ClassLoader уже загрузил класс, то он вернёт байткод без повторной загрузки.
Давайте попробуем с помощью ClassLoader’а загрузить классы.
Для начала нам нужно получить ссылку на системный ClassLoader
Или на ClassLoader нашего класса.
Теперь давайте загрузим класс.
мы получили тот же класс что и раньше, но это далеко не полный список возможностей ClassLoader’а.
loadClass вызывает защищённый
protected synchronized Class loadClass(String name, boolean resolve=false) [ Этот метод также используется Class.forName О нём чуть ниже]
параметр resolve определяет будут ли все классы на которые он ссылается gjlгружены прямо сейчас или отложены до необходимости.
Давайте рассмотрим ещё 1 способ загрузки класса использую Class.forname()
public static Class forName(String name, boolean initialize, ClassLoader loader);
name — имя класса.
initialize — определяет поведение static области класса (выполняемые при его загрузки) static <… инструкции. >
-если true, то инициализация будет выполнена немедленно, иначе будет отложена до первого обращения к классу.
loader — класслоадер.
Как может показаться это равносильно loader.loadClass(name), но всё-таки следует использовать Class.forName
т к это метод делает дополнительные манипуляции с классом, а также кэширует его,
обеспечивая адекватную работу даже при неаккуратной загрузчика loader.
И protected способ ClassLoader’а:
protected final Class defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
name — ожидаемое имя класса, или null если неизвестно.
b — байткод класса, например считанный с FileInputStream
off — offset чтения буфера
len — длина для чтения
бросает:
ClassFormatError — Если данные — не валидный класс.
SecurityException — Если была попытка добавить класс в пакет который имеет особый сертификат безопасности ProtectionDomain(это отдельная тема), или в системный пакет java.*.
Чувствителный к ProtectionDomain метод protected final Class defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain)
Перед тем как использовать данный класс его нужно resolve’ить(аналогично loadClass(String name, boolean resolve))
Подробнее о ClassLoader’е можно прочитать здесь: piarmedia.ru/?page_id=9
=====Часть 3=====
====Практика====
Пишем модуль плагинов/модов
Как ясно из части 2 есть 2 основных способы гибкой динамической загрузки класса:
1. Собственный ClassLoader, но это сложный путь.
—Так как загруженные классы будут обращаться к вам же за загрузкой других классов в т ч и системных.
2. Получение метода defineClass и resolve через reflection и их использование.
Я поддерживаю второй вариант им мы и будем пользоваться.
За основу возьмём такой класс:
IPlugin — интерфейс главного класса плагина.
Например такой примитивный:
Давайте начнём: в конструкторе мы будем получать ссылку на защищённые методы defineclass и resolveclass:
Теперь пишем реализацию loadPlugins()
И на этом всё, Спасибо за просмотр.
Если у вас возникли какие-то вопросы — задавайте.
Классификация и функции загрузчиков классов
Введение
Как известно, Программы на Java транслируются в байт-код, выполняемый виртуальной машиной Java (JVM) — программой, обрабатывающей байтовый код и передающей инструкции интерпретатору. Понятно, что прежде чем интерпретировать байт-код, его необходимо загрузить в оперативную память компьютера. Итак, как же загружается самый первый класс?
Все классы в Java загружаются с помощью загрузчиков классов. Вначале работы программы создается 3 основных загрузчика классов:
Помимо основных загрузчиков классов, существует возможность создания пользовательских загрузчиков классов. О них мы поговорим позже.
Тогда зачем же нужны остальные загрузчики, если базовый загрузчик и так неплохо выполняет свою работу? Зачем понадобилось разбивать процедуру загрузки классов на несколько этапов? Чтобы ответить на этот вопрос, нужно рассмотреть остальные загрузчики классов и их взаимодействие.
Итак, загрузчик расширений – загружает различные пакеты расширений, которые располагаются в директории /lib/ext или другой директории, описанной в системном параметре java.ext.dirs. Это позволяет обновлять и добавлять новые расширения без необходимости модифицировать настройки используемых приложений. Загрузчик расширений реализован классом sun.misc.Launcher$ExtClassLoader.
И, наконец, системный загрузчик – загружает классы, пути к которым указаны в переменной окружения CLASSPATH или пути, которые указаны в командном рядке после ключей –classpath или –cp. Системный загрузчик реализован классом sun.misc.Launcher$AppClassLoader.
Принцип работы загрузчиков классов
Каждый загрузчик классов (кроме Bootstrap) имеет родительский загрузчик, и в большинстве случаев он запрашивает родительского загрузчика загрузить указанный класс, перед тем как попробовать загрузить его самостоятельно.
Существует так же явный способ инициировать загрузку требуемого класса. Явное инициирование выполняться с помощью методов ClassLoader.loadClass() или Class.forName(). Например явное инициирование используется при загрузке JDBC драйверов: Class.forName(«oracle.jdbc.driver.OracleDriver»);
Иерархия загрузчиков классов выглядит следующим образом:
| 1 Bootstrap |
| 2 Extensions |
| 3 Application |
| 4 Пользовательский (если существует) |
Что бы получить загрузчик класса в коде, нужно воспользоваться методом getClassLoader(), например:
public class ClassLoadersTest <
public static void main ( String [ ] args ) <
Вызов i.getClass().getClassLoader() вернет null, что свидетельствует о том, что класс был загружен именно базовым загрузчиком.
Давайте рассмотрим процесс загрузки классов более детально. Допустим, у нас есть некий класс, который мы будем загружать (например Integer). Процесс загрузки будет следующим:
Таким образом, процесс загрузки имеет одно важное свойство, а именно делегирование (рисунок 1). Это позволяет загружать классы тем загрузчиком, который находится ближе всего к базовому в иерархии делегирования. Как следствие поиск классов будет происходить в источниках в порядке их доверия: сначала в библиотеке core API, потом в папке расширений, потом в локальных файлах classpath.
Другими словами, если бы вы скачали стороннюю библиотеку с классом Integer и указали ее в переменной пути, то загрузился бы все равно оригинальный Integer(целое число).
Пользовательские загрузчики классов
В Java существует возможность создания собственных загрузчиков классов. Это может быть полезно, когда нет возможности или нежелательно перечислять все используемые библиотеки при старте программы в CLASSPATH. Например, в программе должна быть возможность динамической загрузки плагинов. Или возможностей стандартного загрузчика недостаточно для загрузки нужных классов.
Собственные загрузчики классов используют все серверы приложений и web-контейнеры, что и понятно – приложения, разворачиваемые на сервере приложений, должны загружаться динамически, в противном случае перечисление в переменной CLASSPATH всех библиотек, используемых приложениями, становится задачей нетривиальной.
Процесс создания собственного загрузчика хорошо описан в статье: http://samolisov.blogspot.com/2008/01/java.html
Процесс загрузки класса более детально
Процесс загрузки класса состоит из трех частей:
Loading – на этой фазе происходит поиск и физическая загрузка файла класса в определенном источнике (в зависимости от загрузчика). Этот процесс определяет базовое представление класса в памяти. На этом этапе такие понятия как методы, поля и т.д. пока не известны.
Linking – процесс, который может быть разбит на 3 части:
Initialization – происходит выполнение статических инициализаторов определенных в классе. Таким образом, статические поля инициализируются стандартными значениями.
Исключения
При работе загрузчиков классов наиболее часто встречаются следующие исключительные ситуации:
1. ClassNotFoundException, бросается, когда приложение пытается загрузиться класс по его названию (String) с помощью таких средств:
Но класса с таким именем не существует.
2. NoClassDefFoundError, бросается в таких случаях:
Иногда проблемы, связанные с загрузкой класса, проявляются не только на этапе загрузки, но и на этапе использования класса.
Выгрузка классов
В большинстве случаев, жизненный цикл класса в виртуальной машине схож с жизненным циклом объекта. JVM загружает, связывает и инициализирует классы, позволяя программе пользоваться ими, и выгружает, когда в приложении они более не используется. Важно заметить, что выгрузка классов не работает в том случае, если класс был загружен Bootstrap загрузчиком.
Загруженные классы, несмотря на то, что являются полноценными Java-объектами, хранятся в особой системной области памяти, называемой permament generation (сокращенно, PermGen) и управляемой сборщиком мусора.
Выгрузка классов является важной частью механизма работы JVM, поскольку Java программы могут динамически расширяться во время работы, загружая пользовательские классы и, тем самым, занимать много места в оперативной памяти. Держать классы в памяти, которые больше не будут использоваться, нет никакого смысла.
Конкретная политика выгрузки классов во многом зависит от реализации виртуальной машины JVM.
Заключение
Таким образом, мы с вами немного приблизились к пониманию процесса загрузки классов в JVM. Были рассмотрены типы загрузчиков, их иерархия, фазы загрузки класса и исключительные ситуации, которые могут возникнуть в процессе.
Если Вам понравилась статья, проголосуйте за нее
Голосов: 81 Голосовать




