Основы динамической загрузки классов в Java
Основа работы с классами в Java—загрузчики, обычные Java-объекты, предоставляющие интерфейс для поиска и создания объекта класса по его имени во время работы приложения.
Динамическая загрузка классов в Java имеет ряд особенностей:
- отложенная (lazy) загрузка и связывание классов
Загрузка классов производится только при необходимости, что позволяет экономить ресурсы и распределять нагрузку. - проверка корректности загружаемого кода (type safeness)
Все действия, связанные с контролем использования типов, производятся только во время загрузки класса, позволяя избежать дополнительной нагрузки во время выполнения кода. - программируемая загрузка
Пользовательский загрузчик полностью контроллирует процесс получения запрошенного класса—самому ли искать байт-код и создавать класс или делегировать создание другому загрузчику. Дополнительно существует возможность выставлять различные атрибуты безопасности для загружаемых классов, позволяя таким образом работать с кодом из ненадежных источников. - множественные пространства имен
Каждый загрузчик имеет своё пространство имён для создаваемых классов. Соответственно, классы, созданные двумя различными загрузчиками на основе общего байт-кода, в системе будут различаться.
Модель динамической загрузки
Во время работы приложения, не только система, но и пользователь (расширяя функциональность класса java.lang.ClassLoader) имеет возможность создавать загрузчики классов. Связь между различными загрузчиками регламентируется моделью делегирования загрузки.
Модель делегирования загрузки представляет собой дерево, описывающее связь «родитель-потомок» между представленными загрузчиками. Каждый загрузчик, за исключением базового, должен иметь родительский загрузчик, причем единственный. Дочерний загрузчик может направить (делегировать) запрос на создание класса своему родительскому загрузчику. Для пользовательского загрузчика родительским по-умолчанию является системный, но ничто не мешает при создании явно указаывать любой другой доступный загручик.

Во время начала работы автоматически создается 3 различных загрузчика классов, отвечающих за загрузку различных компонент системы:
- базовый загрузчик (bootstrap/primodial class loader)
- загрузчик расширений (extention class loader)
- системный загрузчик (system/application class loader)
Основные системные классы, которые обычно находятся в jar-файлах в директории jre/lib загружаются именно базовым загрузчиком. Опция -Xbootclasspath предоставляет возможность модифицировать набор классов, доступных для загрузки базовым загрузчиком.
Ещё одной особенностью базового загрузчика является тот факт, что он не может быть создан из Java-кода. Зачастую, это обуславливается тем, что его реализация является частью виртуальной машины.
Работа с базовым загрузчиком напрямую невозможна. Например, вызов java.lang.Object.class.getClassLoader() вернет null, что свидетельствует о том, что класс был загружен именно базовым загрузчиком.
Загрузчик расширений является дочерним для базового загрузчика. Его основная задача – загрузка различных пакетов расширений, которые обычно помещаются в jre/lib/ext. Это позволяет обновлять и добавлять новые расширения без необходимости модифицировать настройки используемых приложений.
Системный загрузчик ответственнен за загрузку классов из директорий, перечисленных в переменной окружения CLASSPATH. Данный загрузчик можно получить вызвав метод ClassLoader.getSystemClassLoader().
Во время загрузки, поиск запрошенного класса производится в следующей последовательности:
- поиск в списке ранее загруженных классов
Проверяется запрашивался ли ранее данный класс, и, если да, возвращается тот же самый класс, что и ранее. - делегирование родительскому загрузчику
В случае, если класс ранее не был загружен, запрос на загрузку делегируется родительскому загрузчику. Это позволяет загружать классы тем загрузчиком, который находится ближе всего к базовому в иерархии делегирования. - попытаться загрузить класс самому
Если родительский загрузчик не смог загрузить запрошенный класс, текущий загрузчик пытается сам произвести процесс загрузки требуемого класса: найти байт-код и на основе него создать требуемый класс.
Пользовательский загрузчик может не придерживаться данной последовательности шагов, но это чревато появлением трудноотловимых ошибок, связанных с ограничением области видимости загружаемых классов.
Как следствие, все системные классы загружаются базовым загрузчиком (например, java.lang.Object, java.lang.String и т.д.). Более того, ряд системных классов, по соображениям безопасности, могут быть загружены только лишь базовым загрузчиком - любые попытки создать один из таких классов другим загрузчиком завершатся неудачно.
Загрузчик, подчиняющийся данной модели делегирования, имеет доступ только к тем классам, которые загружены им самим или другими загрузчиками, находящимися выше него в цепочке делегирования.
С учетом правил делегирования, класс может быть создан загрузчиком, отличным от того: который инициировал его загрузку. Поэтому для каждого конкретного класса особое значение имеют два загрузчика—инициировавший загрузку (initiating loader) и непосредственно создавший его (defining loader).
Если все загрузчики корректно реализуют модель делегирования, с учетом правила «сначала предлагать загрузить класс родителю, а потом пытаться сделать это самому», базовый загрузчик будет первым кто попытается найти и создать класс, а потом данную операцию последовательно вниз по пути делегирования будут пытаться осуществить остальные загрузчики вплоть до инициировавшего загрузку. Это позволяет создавать класс тем загрузчиком, который наиболее близко находится к базовому, тем самым максимизируя область видимости класса.

Процесс создания класса
После того, как байт-код для запрошенного класса был найден, необходимо на его основе создать класс - получить полноценный объект, представляющий Java-класс. Для этого используется метод ClassLoader.defineClass().
Процесс конструирования класса состоит из ряда последовательных фаз:
- Верификация байт-кода (verification)
Проверка переданного кода на соответствие ряду зачастую нетривиальных требований определенных в спецификации JVM. - Предварительная подготовка к загрузке (preparation)
Создание и инициализация необходимых структур, используемых для представления полей, методов, реализованных интерфейсов и т.п., определенных в загружаемом классе. - Анализ зависимостей (resolution)
Загрузка набора классов, на которые ссылается загружаемый класс. Например:- родительский класс
- реализованные интерфейсы
- поля класса
- типы, используемые в сигнатурах методов класса
- локальные переменные в методах класса
- Инициализация
Вызов статических блоков инициализации и присваивание полям класса значений по-умолчанию.
По завершению всех перечисленных фаз, класс полностью готов к использованию.
Если виртуальной машине передать параметр -verbose:class, то информация о каждой успешной загрузке класса будет выводиться на консоль.
Например, запустив java -verbose:class -help получим:
[Opened /opt/sun-jdk-1.5.0.06/jre/lib/rt.jar] ... список открываемых jar-файлов... [Opened /opt/sun-jdk-1.5.0.06/jre/lib/charsets.jar] [Loaded java.lang.Object from /opt/sun-jdk-1.5.0.06/jre/lib/rt.jar] ... список загружаемых классов и путь откуда был получен файл класса... [Loaded java.lang.SystemClassLoaderAction from /opt/sun-jdk-1.5.0.06/jre/lib/rt.jar] Usage: java [-options] class [args...] ... информация о допустимых параметрах ... [Loaded java.lang.Shutdown from /opt/sun-jdk-1.5.0.06/jre/lib/rt.jar] [Loaded java.lang.Shutdown$Lock from /opt/sun-jdk-1.5.0.06/jre/lib/rt.jar]
Явное и неявное инициирование загрузки класса
Существует несколько способов инициировать загрузку требуемого класса:
- явное инициирование
Вызов ClassLoader.loadClass() или Class.forName(). - неявное инициирование
Виртуальная машина, когда для дальнейшей работы приложения требуется ранее не использованный класс, инициирует его загрузку.
В случае, если вызывается метод Class.forName(), по умолчанию используется загрузчик, создавший текущий класс. Хотя есть возможность и явно указать желаемый загрузчик.
Во время загрузки классов возможно комбинирование обоих способов. Например, сначала загрузка явно инициируется в приложении, а в дальнейшем, по мере необходимости, неявно подгружаются остальные классы.
Выгрузка ранее загруженных классов
В общем случае, класс может быть выгружен только тогда, когда в приложении он более не используется. Конкретная же политика выгрузки классов во многом зависит от реализации виртуальной машины и в дальнейшем будет описываться поведение Sun HotSpot JVM.
Загруженные классы, несмотря на то, что являются полноценными Java-объектами, хранятся в особой системной области памяти, называемой permament generation (сокращенно, PermGen) и управляемой сборщиком мусора. Соответственно, класс будет выгружен только тогда, когда на него не осталось ссылок. В случае, если класс был создан пользовательским загрузчиком, прямая ссылка на него храниться загрузчиком и класс может быть выгружен только после успешной выгрузки загрузчика. Это правило распространяется и на загрузчики, управляемые системой. Соответственно, классы, созданные базовым загрузчиком, не могут быть выгружены в принципе. На практике это верно и для системного загрузчика с загрузчиком расширений—их выгрузка во время работы приложения не предусмотрена.
Параметр -verbose:class также позволяет получить информацию и о выгружаемых классах.
Владимир Иванов
опубликовал vmrobot ( июл 03 2006, 10:16:20 PM MSD ) Permalink Комментарии [10]


...В случае, если класс был создан пользовательским загрузчиком, прямая ссылка на него храниться загрузчиком и класс может быть выгружен только после успешной выгрузки загрузчика. На классы, созданные системным загрузчиком, это правило не распространяется (иначе их просто нельзя было бы выгрузить) и они выгружаются при сборке мусора в PermGen, если на них не осталось ссылок.
Интересно, откуда была взята информация о системном загрузчике? Мне всегда казалось, что он должен вести себя точно также как и любые другие пользовательские загрузчики.
В принципе, если бы классы, загруженные системным загрузчиком, могли быть выгружены, то это могло бы привести к тому, что статический инициализатор для какого-то класса мог бы быть вызван несколько раз за время работы программы, что, в свою очередь, могло бы приводить к весьма неожиданным результатам.
опубликовал dshe Июль 27, 2006 at 04:36 PM MSD #
опубликовал Владимир Иванов Август 07, 2006 at 02:21 PM MSD #
Перезагрузка класса (которая похоже была до 1.2) приводит к тому, что статические поля теряют свою информацию. Фактически это означает, что такой безобидный код
может вывести не 10, а, скажем, 0. Просто потому, что между записью в статическое поле и его чтением, могла бы произойти сборка, которая сколлектила MyClass.
Сейчас, согласно JVMS:
2.17.8 Unloading of Classes and Interfaces
A class or interface may be unloaded if and only if its class loader is unreachable. The bootstrap class loader is always reachable; as a result, system classes may never be unloaded.
Это достигается тем, что класслоадер содержит ссылки на все свои загруженные классы (можно посмотреть на исходники java.lang.ClassLoader -- там об этом говорится явно), а класс -- на свой класслоадер.
Важно отметить, что для system класслоадера здесь не делается исключение. И в принципе, классы, загруженные system класслоадером (как и любым другим класслоадером, кроме bootstrap), могут быть выгружены. Однако в жизни этого достичь сложно поскольку ссылка на system класслоадер, как минимум, хранится в статическом поле системного класса (т.е. загруженного bootstrap класслоадером): java.lang.ClassLoader.scl
Вот некоторые ссылки по данной теме:
System classes lose static state on class unloading.
Clarifications and Amendments to the JLS
опубликовал dshe Август 08, 2006 at 11:19 AM MSD #
опубликовал Владимир Иванов Август 08, 2006 at 01:35 PM MSD #
опубликовал docker Сентябрь 18, 2006 at 12:45 AM MSD #
Насколько я знаю, импорты никак не обрабатываются на моменте исполнения. Директива import служит только в момент компиляции текста программы. В байт-коде присутствуют только full-qualified class names.
опубликовал null Август 09, 2007 at 01:08 PM MSD #
Привет! Не подскажете, чем отличается обычная загрузка класса от явной Class.forName()? Не может ли использование явной загрузки привести к тому, что память permament generation исчерпается? Насколько я вижу по исходному коду - Class.forName просто вызывает нативный метод. Если, например, постоянно вызывать этот метод для одного и того же имени не приведет ли это к OutOfMemory: PermGen Space?
опубликовал nata Сентябрь 21, 2007 at 08:51 PM MSD #
Насколько я понял, под "обычной" подразумевается вызов метода ClassLoader.loadClass(...). Отличие Class.forName() в том, что в зависимости от конкретного объекта Class, загрузка может быть инициирована различными class loader'ами. В первом случае, загрузка будет инициироваться загрузчиком, метод которого вызывается.
В связи с тем, что в рамках одного загрузчика имя загружаемого класса должно быть уникально, переполнения PermGen'а не произойдет. При первом вызове, если это возможно, класс будет загружен, а при всех последующих будет возвращаться тот же самый класс.
опубликовал Владимир Иванов Сентябрь 22, 2007 at 01:14 AM MSD #
В вашем материале я нашел несколько неточностей (кхе, кхе)
во первых формулировка
----
Процесс конструирования класса состоит из ряда последовательных фаз:
.... много букав .....
Инициализация
Вызов статических блоков инициализации и присваивание полям класса значений по-умолчанию.
----
если быть до конца верным, то стадия инициализации статических переменных и уж тем более значений класса по умолчанию никогда не делается на стадии чтения класса. Статика инициализируется при первом обращении к содержимому класса - к статическому полю или методу. А про обычные поля - даже говорить смешно. Можно было бы возразить, что при использовании стандартного загрузчика классов разницы никакой не будет, но это не так. Но если писать собственные загрузчики, то такое заблуждение пагубно.
опубликовал black Январь 13, 2008 at 11:40 PM MSK #
black: Спасибо большое за конструктивные замечания.
Подмеченные вами неточности порождены скорее терминологическими разногласиями. "Процесс конструирования класса" не ограничивается только лишь "чтением представления класса". В зависимости от реализации, инициализация внутренних структур представляющих загруженный класс может происходить как сразу, так и быть отложенной до первого использования класса. В любом случае, класс не считается загруженным и не может быть использованным до тех пор, пока его инициализация не закончена. И виртуальная машина, в соответствии со спецификацией, должна это гарантировать.
По поводу "обычных полей" - фраза "присваивание полям класса значений по-умолчанию" подразумевает инициализацию статических полей, и не несет никакой информации об инициализации объектов загруженного типа. Каюсь, неясно выразился =)
опубликовал Владимир Иванов Февраль 03, 2008 at 09:03 PM MSK #