JavaRush /Java блог /Random UA /Як відбувається завантаження класів у JVM
Aleksandr Zimin
1 рівень
Санкт-Петербург

Як відбувається завантаження класів у JVM

Стаття з групи Random UA
Після того, як найскладніша частина в роботі програміста виконана і додаток «Hello World 2.0» написано, залишилося зібрати дистрибутив і передати його замовнику, чи хоча б у службу тестування. У дистрибутиві у нас все як належить і, запускаючи нашу програму, на сцену виходить Java Virtual Machine. Ні для кого не секрет, що віртуальна машина зчитує команди, представлені у class-файлух у вигляді байт-коду та транслює їх у вигляді інструкцій процесору. Пропоную трохи розібратися у схемі влучення байт-коду у віртуальну машину.

Завантажувач класів

Використовується для постачання в JVM скомпілюваного байт-коду, який, як правило, зберігається у файлух з розширенням .class, але може бути також отриманий з інших джерел, наприклад, завантажений по мережі або згенерований самим додатком. Як відбувається завантаження класів у JVM - 1Відповідно до специфікації Java SE для того, щоб отримати код, що працює в JVM, необхідно виконати три етапи:
  • завантаження байт-коду з ресурсів та створення екземпляра класуClass

    сюди входить пошук запитаного класу серед завантажених раніше, отримання байт-коду для завантаження та перевірка його коректності, створення екземпляра класу Class(для роботи з ним в runtime), завантаження батьківських класів. Якщо батьківські класи та інтерфейси були завантажені, те й аналізований клас вважається не завантаженим.

  • зв'язування (або лінківка)

    за специфікацією цей етап розбивається ще на три стадії:

    • Verification відбувається перевірка коректності отриманого байт-коду.
    • Preparation , Виділення оперативної пам'яті під статичні поля та ініціалізація їх значеннями за умовчанням (при цьому явна ініціалізація, якщо вона є, відбувається вже на етапі ініціалізації).
    • Resolution , дозвіл символьних посилань типів, полів та методів.
  • ініціалізація отриманого об'єкта

    тут, на відміну від попередніх пунктів, начебто все зрозуміло, що має відбуватися. Було б, звичайно, цікаво розібратися, як саме це відбувається.

Усі ці етапи виконуються послідовно з такими вимогами:
  • Клас повинен бути повністю завантажений перш, ніж слинкований.
  • Клас повинен бути повністю перевірений та підготовлений перш ніж проініціалізований.
  • Помилки дозволу посилань відбуваються під час виконання програми, навіть якщо були виявлені на етапі лінківки.
Як відомо, в Java реалізовано відкладене (або лінива) завантаження класів. А це означає, що завантаження класів посилальних полів класу, що завантажується, не буде виконуватися доти, поки в додатку не зустрінеться явне до них звернення. Іншими словами, дозвіл символьних посилань не є обов'язковим і за умовчанням не відбувається. Проте, у JVM може використовуватися і енергійна завантаження класів, тобто. всі символьні посилання мають бути враховані одразу. Ось для цього пункту і діє остання вимога. Ще варто зауважити, що роздільна здатність символьних посилань не прив'язана до жодного з етапів завантаження класу. Загалом кожен із цих етапів тягне на непогане таке дослідження, спробуємо розібратися з першим, а саме завантаженням байт-коду.

Типи завантажувачів Java

У Java існує три стандартні завантажувачі, кожен з яких здійснює завантаження класу з певного місця:
  1. Bootstrap – базовий завантажувач, також називається Primordial ClassLoader.

    завантажує стандартні класи JDK із архіву rt.jar

  2. Extension ClassLoader – завантажувач розширень.

    завантажує класи розширень, які за умовчанням перебувають у каталозі jre/lib/ext, але можуть бути задані системною властивістю java.ext.dirs

  3. System ClassLoader – системний завантажувач.

    завантажує класи програми, визначені в змінному середовищі оточення CLASSPATH

У Java використовується ієрархія завантажувачів класів, де кореневим є базовий. Далі слідує завантажувач розширень, а за ним вже системний. Звичайно, кожен завантажувач зберігає покажчик на батьківський для того, щоб змогти делегувати йому завантаження в тому випадку, якщо сам не зможе цього зробити.

Анотація класу ClassLoader

Кожен завантажувач, крім базового, є нащадком абстрактного класу java.lang.ClassLoader. Наприклад, реалізацією завантажувача розширень є клас sun.misc.Launcher$ExtClassLoader, а системного завантажувача - sun.misc.Launcher$AppClassLoader. Базовий завантажувач є нативним і його реалізація включена до JVM. Будь-який клас, який розширює java.lang.ClassLoader, може надати свій спосіб завантаження класів з блек-джеком та цими самими. І тому необхідно перевизначити відповідні методи, які зараз можу розглянути лише поверхово, т.к. не розбирався детально у цьому питанні. Ось вони:
package java.lang;
public abstract class ClassLoader {
    public Class<?> loadClass(String name);
    protected Class<?> loadClass(String name, boolean resolve);
    protected final Class<?> findLoadedClass(String name);
    public final ClassLoader getParent();
    protected Class<?> findClass(String name);
    protected final void resolveClass(Class<?> c);
}
loadClass(String name)один із небагатьох публічних методів, який і є точкою входу для завантаження класів. Його реалізація зводиться до виклику іншого protected методу loadClass(String name, boolean resolve), його необхідно перевизначити. Якщо подивитися Javadoc цього захищеного методу, можна зрозуміти приблизно таке – на вхід подаються два параметри. Один це бінарне ім'я класу (або цілком певне ім'я класу), який потрібно завантажити. Назва класу вказується з перерахуванням усіх пакетів. Другий параметр – це прапор, який визначає, чи потрібно виконувати процедуру вирішення символьних посилань. За умовчанням він дорівнює false , що означає використання лінивого завантаження класів. Далі, згідно з документацією, у реалізації методу за умовчанням відбувається викликfindLoadedClass(String name), який перевіряє чи був клас вже завантажений раніше і якщо це так, поверне посилання на цей клас. Інакше буде викликано метод завантаження класу у батьківського завантажувача. Якщо жоден із завантажувачів не зміг знайти завантажений клас, кожен із них, слідуючи у зворотному порядку, спробує цей клас знайти та завантажити, перевизначаючи метод findClass(String name). Докладніше про це буде розглянуто у розділі «Схема завантаження класів». І нарешті, в останню чергу, після того, як клас вдалося завантажити, залежно від прапора resolve буде вирішено, чи варто виконувати завантаження класів за символьними посиланнями. Явний приклад того, що стадія Resolution може бути викликана етапі завантаження класу. Відповідно, розширюючи класClassLoaderі перевизначаючи його методи, завантажувач користувача може здійснювати свою логіку поставки байт-коду у віртуальну машину. Також Java підтримується поняття «поточного» завантажувача класів. Поточний завантажувач це той, який завантажив клас, який зараз виконується. Кожен клас знає, яким завантажувачем він був завантажений, і можна отримати цю інформацію, викликавши метод String.class.getClassLoader(). Для всіх класів програми "поточний" завантажувач, як правило, системний.

Три принципи завантаження класів

  • Делегування

    Запит на завантаження класу передається батьківському завантажувачу, і спроба завантажити клас самостійно виконується, лише якщо батьківський завантажувач не зміг знайти та завантажити клас. Такий підхід дозволяє завантажувати класи завантажувачем, який максимально близько знаходиться до базового. Так досягається максимальна область видимості класів. Кожен завантажувач веде облік класів, які були завантажені саме ним, поміщаючи в свій кеш. Безліч цих класів і називається областю видимості.

  • Видимість

    Завантажувач бачить лише «свої» класи та класи «батька» і не має поняття про класи, які були завантажені його «нащадком».

  • Унікальність

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

Таким чином, при написанні завантажувача розробник повинен керуватися цими трьома принципами.

Схема завантаження класів

Коли відбувається завантаження будь-якого класу, відбувається пошук цього класу в кеші вже завантажених класів поточного завантажувача. Якщо бажаний клас ще завантажувався раніше, за принципом делегування управління передається батьківському завантажувачу, який перебуває за ієрархією на рівень вище. Батьківський завантажувач також намагається знайти бажаний клас у себе в кеші. Якщо клас вже був завантажений і завантажувач знає про його місцезнаходження, то буде повернуто об'єктClassцього класу. Якщо ні, пошук буде продовжуватися доти, доки не дійде до базового завантажувача. Якщо і в базовому завантажувачі немає інформації про клас, що шукається (тобто він ще не був завантажений), буде виконано пошук байт-коду цього класу за розташуванням класів, про який знає даний завантажувач, і, якщо завантажити клас не вдасться, управління повернеться назад завантажувачу-нащадку, який намагатиметься виконати завантаження із відомих йому джерел. Як згадувалося вище, розташування класів для базового завантажувача це бібліотека rt.jar, для завантажувача розширень – каталог із розширеннями jre/lib/ext, для системного – CLASSPATH, для користувача це може бути щось своє. Таким чином, хід завантаження класів йде у зворотному напрямку – від кореневого завантажувача до поточного. Коли байт-код класу знайдено,Class. Як неважко помітити, описана схема завантаження схожа на наведену реалізацію методу loadClass(String name). Нижче можна розглянути цю схему на діаграмі.
Як відбувається завантаження класів у JVM - 2

Як висновок

На перших кроках вивчення мови немає якоїсь особливої ​​необхідності у розумінні того, як відбувається завантаження класів у Java, але знання цих базових принципів дозволить не впадати у відчай, зустрівши такі помилки, як ClassNotFoundExceptionабо NoClassDefFoundError. Ну чи хоча б приблизно розуміти, у чому корінь проблеми. Так виняток ClassNotFoundExceptionвиникає при динамічному завантаженні класу під час виконання програми, коли завантажувачі не можуть знайти необхідний клас ні в кеші, ні шляхом знаходження класів. А ось помилкаNoClassDefFoundErrorє критичнішою і виникає в тому випадку, коли під час компіляції шуканий клас був доступний, але не видно під час виконання програми. Це може статися, якщо в поставку програми забули увімкнути бібліотеку, яку вона використовує. Ну і сам факт розуміння принципів пристрою того інструменту, яким користуєшся в роботі (не обов'язково чітке і детальне занурення в його надра), додає деяку ясність у розумінні процесів, що протікають всередині цього механізму, що, у свою чергу, веде до впевненого використання цього інструменту.

Джерела

How ClassLoader Works in Java Загалом дуже корисне джерело з доступним викладом інформації. Завантаження класів, ClassLoader Досить об'ємна стаття, але з ухилом на те, як зробити свою реалізацію завантажувача з цими самими. ClassLoader: динамічне завантаження класів На жаль, цей ресурс зараз недоступний, але там я знайшов найзрозумілішу діаграму зі схемою завантаження класів, тому не можу не додати. Java SE Specification: Chapter 5. Loading, Linking, and Initializing
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ