Введение
Как известно, Программы на Java транслируются в байт-код, выполняемый виртуальной машиной Java (JVM) — программой, обрабатывающей байтовый код и передающей инструкции интерпретатору. Понятно, что прежде чем интерпретировать байт-код, его необходимо загрузить в оперативную память компьютера. Итак, как же загружается самый первый класс?
Все классы в Java загружаются с помощью загрузчиков классов. Вначале работы программы создается 3 основных загрузчика классов:
-
базовый загрузчик (bootstrap)
-
загрузчик расширений (extention)
-
системный загрузчик (system/application)
Помимо основных загрузчиков классов, существует возможность создания пользовательских загрузчиков классов. О них мы поговорим позже.
Загрузчики классов являются иерархическими. Загрузчик, который загружает основные системные классы, называется базовым (Bootstrap или Primordial) загрузчиком классов. Именно он загружает внутренние классы JDK и пакеты java.* (rt.jar и i18n.jar) . Важно заметить, что базовый загрузчик является «Изначальным или Корневым» и частью JVM, вследствие чего его нельзя создать внутри кода программы.
Тогда зачем же нужны остальные загрузчики, если базовый загрузчик и так неплохо выполняет свою работу? Зачем понадобилось разбивать процедуру загрузки классов на несколько этапов? Чтобы ответить на этот вопрос, нужно рассмотреть остальные загрузчики классов и их взаимодействие.
Итак, загрузчик расширений – загружает различные пакеты расширений, которые располагаются в директории <JAVA_HOME>/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 static void main(String[] args){
Integer i = 23;
System.out.println(i.getClass().getClassLoader());
}
}
Вызов i.getClass().getClassLoader() вернет null, что свидетельствует о том, что класс был загружен именно базовым загрузчиком.
Давайте рассмотрим процесс загрузки классов более детально. Допустим, у нас есть некий класс, который мы будем загружать (например Integer). Процесс загрузки будет следующим:
-
Cистемный загрузчик (sun.misc.Launcher$AppClassLoader) проверит, не загружался ли данный класс ранее. Если он уже загружался, то возвращается данный класс из кэша. Если нет, системный загрузчик делегирует поиск класса родительскому классу-загрузчику.
-
Загрузчик расширений (sun.misc.Launcher$ExtClassLoader) выполняет такую же процедуру
-
Наконец, базовый загрузчик(bootstrap), загружает класс Integer самостоятельно, поскольку у него нет родительского класса.
Таким образом, процесс загрузки имеет одно важное свойство, а именно делегирование (рисунок 1). Это позволяет загружать классы тем загрузчиком, который находится ближе всего к базовому в иерархии делегирования. Как следствие поиск классов будет происходить в источниках в порядке их доверия: сначала в библиотеке core API, потом в папке расширений, потом в локальных файлах classpath.
Другими словами, если бы вы скачали стороннюю библиотеку с классом Integer и указали ее в переменной пути, то загрузился бы все равно оригинальный Integer(целое число).
Еще одно важное свойство - каждый загрузчик имеет свое пространство имен для создаваемых классов. Т.е. если классы одинаковы и находятся в одном пакете, но загружаются разными загрузчиками - они считаются разными. Таким образом, можно например, создать два объекта синглтона, если постараться :)
Пользовательские загрузчики классов
В Java существует возможность создания собственных загрузчиков классов. Это может быть полезно, когда нет возможности или нежелательно перечислять все используемые библиотеки при старте программы в CLASSPATH. Например, в программе должна быть возможность динамической загрузки плагинов. Или возможностей стандартного загрузчика недостаточно для загрузки нужных классов.
Собственные загрузчики классов используют все серверы приложений и web-контейнеры, что и понятно – приложения, разворачиваемые на сервере приложений, должны загружаться динамически, в противном случае перечисление в переменной CLASSPATH всех библиотек, используемых приложениями, становится задачей нетривиальной.
За создание пользовательских загрузчиков классов отвечает класс ClassLoader. Для того, что бы создать собственный загрузчик классов, необходимо унаследоваться от класса ClassLoader .
Процесс создания собственного загрузчика хорошо описан в статье: http://samolisov.blogspot.com/2008/01/java.html
Процесс загрузки класса более детально
Процесс загрузки класса состоит из трех частей:
1. Loading
2. Linking
3. Initialization
Loading – на этой фазе происходит поиск и физическая загрузка файла класса в определенном источнике (в зависимости от загрузчика). Этот процесс определяет базовое представление класса в памяти. На этом этапе такие понятия как методы, поля и т.д. пока не известны.
Linking – процесс, который может быть разбит на 3 части:
-
Bytecode verification – происходит несколько проверок байт-кода на соответствие ряду зачастую нетривиальных требований определенных в спецификации JVM (http://java.sun.com/docs/books/vmspec/).
-
Class preparation – на этом этапе происходит подготовки структуры данных, отображающей поля, методы и реализованные интерфейсы, которые определены в классе.
-
Resolving – разрешение все классов, которые ссылаются на текущий класс.
Initialization – происходит выполнение статических инициализаторов определенных в классе. Таким образом, статические поля инициализируются стандартными значениями.
Детально о фазах загрузки: http://www.artima.com/insidejvm/ed2/lifetype.html
Исключения
При работе загрузчиков классов наиболее часто встречаются следующие исключительные ситуации:
1. ClassNotFoundException, бросается, когда приложение пытается загрузиться класс по его названию (String) с помощью таких средств:
-
forName метод в классе Class.
-
findSystemClass метод в классе ClassLoader.
-
loadClass метод в классе ClassLoader.
Но класса с таким именем не существует.
2. NoClassDefFoundError, бросается в таких случаях:
-
Когда архив, директория, или другой источник необходимых классов не был добавлен в источники текущего загрузчика классов или его предка.
-
Загрузчик-предок не был установлен корректно.
Иногда проблемы, связанные с загрузкой класса, проявляются не только на этапе загрузки, но и на этапе использования класса.
Выгрузка классов
В большинстве случаев, жизненный цикл класса в виртуальной машине схож с жизненным циклом объекта. JVM загружает, связывает и инициализирует классы, позволяя программе пользоваться ими, и выгружает, когда в приложении они более не используется. Важно заметить, что выгрузка классов не работает в том случае, если класс был загружен Bootstrap загрузчиком.
Загруженные классы, несмотря на то, что являются полноценными Java-объектами, хранятся в особой системной области памяти, называемой permament generation (сокращенно, PermGen) и управляемой сборщиком мусора.
Выгрузка классов является важной частью механизма работы JVM, поскольку Java программы могут динамически расширяться во время работы, загружая пользовательские классы и, тем самым, занимать много места в оперативной памяти. Держать классы в памяти, которые больше не будут использоваться, нет никакого смысла.
Конкретная политика выгрузки классов во многом зависит от реализации виртуальной машины JVM.
Заключение
Таким образом, мы с вами немного приблизились к пониманию процесса загрузки классов в JVM. Были рассмотрены типы загрузчиков, их иерархия, фазы загрузки класса и исключительные ситуации, которые могут возникнуть в процессе.