Перейти к содержанию

Загрузчики классов (Class Loaders) в Java

Загрузчики классов отвечают за динамическую загрузку классов Java в JVM (виртуальную машину Java) во время выполнения. Они также являются частью JRE (Java Runtime Environment). Следовательно, JVM не нужно знать о базовых файлах или файловых системах для запуска программ Java благодаря загрузчикам классов.

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

В этом руководстве поговорим о различных типах встроенных загрузчиков классов и о том, как они работают. Затем представим собственную реализацию.

Типы встроенных загрузчиков классов

Начнем с изучения того, как можно загружать разные классы, используя различные загрузчики классов:

public void printClassLoaders() throws ClassNotFoundException {

    System.out.println("Classloader of this class:"
        + PrintClassLoader.class.getClassLoader());

    System.out.println("Classloader of Logging:"
        + Logging.class.getClassLoader());

    System.out.println("Classloader of ArrayList:"
        + ArrayList.class.getClassLoader());
}

При выполнении вышеуказанный метод напечатает следующее:

Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
Class loader of ArrayList:null

Как видим, здесь есть три разных загрузчика классов: приложение (application), расширение (extension) и начальный загрузчик (bootstrap, отображается как null).

Загрузчик класса приложения загружает класс, в котором содержится пример метода. Загрузчик класса приложения или системы загружает наши собственные файлы в путь к классам (classpath).

Затем загрузчик класса расширения загружает класс Logging. Загрузчики классов расширения загружают классы, которые являются расширением стандартных базовых классов Java.

Наконец, загрузчик классов начальной загрузки загружает класс ArrayList. Начальный загрузчик или первичный загрузчик классов является родителем всех остальных.

Однако мы видим, что для ArrayList в выходных данных отображается null. Это связано с тем, что загрузчик классов начальной загрузки написан на нативном коде, а не на Java, поэтому он не отображается как класс Java. В результате поведение загрузчика классов начальной загрузки будет различаться для разных JVM.

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

Первоначальный загрузчик классов (Bootstrap Class Loader)

Классы Java загружаются экземпляром java.lang.ClassLoader. Однако загрузчики классов сами по себе являются классами. Итак, вопрос в том, кто загружает сам java.lang.ClassLoader?

Именно здесь вступает в игру Bootstrap или первоначальный загрузчик классов.

Он в основном отвечает за загрузку внутренних классов JDK, обычно rt.jar и других основных библиотек, расположенных в каталоге $JAVA_HOME/jre/lib. Кроме того, загрузчик классов Bootstrap служит родителем всех остальных экземпляров ClassLoader.

Загрузчик классов Bootstrap является частью основной JVM и написан на машинном коде, как указано в приведенном выше примере. На разных платформах могут быть разные реализации этого конкретного загрузчика классов.

Загрузчик класса расширений (Extension Class Loader)

Загрузчик классов расширений является потомком загрузчика классов Bootstrap и обеспечивает загрузку расширений стандартных классов Java Core, чтобы они были доступны для всех приложений, работающих на платформе.

Загрузчик класса расширений загружается из каталога расширений JDK, обычно это каталог $JAVA_HOME/lib/ext или любой другой каталог, указанный в системном свойстве java.ext.dirs.

Загрузчик классов системы (System Class Loader)

С другой стороны, загрузчик классов системы или приложения заботится о загрузке всех классов уровня приложения в JVM. Он загружает файлы, найденные в переменной среды classpath, -classpath или параметре командной строки -cp. Это также дочерний элемент загрузчика классов расширений.

Как работают загрузчики классов

Загрузчики классов являются частью среды выполнения Java. Когда JVM запрашивает класс, загрузчик классов пытается найти класс и загрузить определение класса в среду выполнения, используя полное имя класса.

Метод java.lang.ClassLoader.loadClass() отвечает за загрузку определения класса в среду выполнения. Он пытается загрузить класс на основе полного имени.

Если класс еще не загружен, он делегирует запрос загрузчику родительского класса. Этот процесс происходит рекурсивно.

В конце концов, если загрузчик родительского класса не найдет класс, то дочерний класс вызовет метод java.net.URLClassLoader.findClass() для поиска классов в самой файловой системе.

Если последний загрузчик дочернего класса также не может загрузить класс, он генерирует исключение java.lang.NoClassDefFoundError или java.lang.ClassNotFoundException. Посмотрим на пример вывода, когда выдается ClassNotFoundException:

java.lang.ClassNotFoundException: com.baeldung.classloader.SampleClassLoader    
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)    
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)    
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)    
    at java.lang.Class.forName0(Native Method)    
    at java.lang.Class.forName(Class.java:348)

Если пройдемся по последовательности событий сразу после вызова java.lang.Class.forName(), то увидим, что сначала он пытается загрузить класс через загрузчик родительского класса, а затем java.net.URLClassLoader.findClass() ищет сам класс.

Если он по-прежнему не находит класс, то генерирует исключение ClassNotFoundException.

Теперь рассмотрим три важные особенности загрузчиков классов.

Модель делегирования

Загрузчики классов следуют модели делегирования, когда при запросе на поиск класса или ресурса экземпляр ClassLoader делегирует поиск класса или ресурса загрузчику родительского класса.

Допустим, есть запрос на загрузку класса приложения в JVM. Загрузчик класса системы сначала делегирует загрузку этого класса своему родительскому загрузчику классов расширений, который, в свою очередь, делегирует его загрузчику классов начальной загрузки.

Только если начальная загрузка, а затем загрузчик классов расширений не могут загрузить класс, загрузчик классов системы пытается загрузить сам класс.

Уникальные классы

Как следствие модели делегирования, легко обеспечить уникальные классы, поскольку делегирование всегда происходит вверх.

Если загрузчик родительского класса не может найти класс, тогда только текущий экземпляр попытается сделать это самостоятельно.

Видимость

Кроме того, загрузчики дочерних классов видны классам, загруженным их загрузчиками родительских классов.

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

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

Однако класс B является единственным классом, видимым для других классов, загружаемых загрузчиком классов расширения.

Пользовательский загрузчик классов

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

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

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

Варианты использования пользовательских загрузчиков классов

Пользовательские загрузчики классов полезны не только для загрузки класса во время выполнения. Несколько вариантов использования могут включать:

  1. Помощь в изменении существующего байт-кода. Например, weaving agents.
  2. Создание классов, динамически подходящих для нужд пользователя. Например, в JDBC переключение между различными реализациями драйверов осуществляется посредством динамической загрузки классов.
  3. Реализацию механизма управления версиями классов при загрузке разных байт-кодов для классов с одинаковыми именами и пакетами. Это можно сделать либо с помощью загрузчика классов URL (загрузка jar-файлов через URL-адреса), либо с помощью пользовательских загрузчиков классов.

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

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

Затем он загружает raw-файлы байт-кода через HTTP и превращает их в классы внутри JVM. Даже если эти апплеты имеют одинаковое имя, они считаются разными компонентами, если загружаются разными загрузчиками классов.

Теперь, когда понимаем, почему пользовательские загрузчики классов важны, реализуем подкласс ClassLoader, чтобы расширить и суммировать функциональные возможности того, как JVM загружает классы.

Создание пользовательского загрузчика классов

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

Для этого нужно расширить класс ClassLoader и переопределить метод findClass():

public class CustomClassLoader extends ClassLoader {

    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassFromFile(name);
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassFromFile(String fileName)  {
        InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
                fileName.replace('.', File.separatorChar) + ".class");
        byte[] buffer;
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        int nextValue = 0;
        try {
            while ( (nextValue = inputStream.read()) != -1 ) {
                byteStream.write(nextValue);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        buffer = byteStream.toByteArray();
        return buffer;
    }
}

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

Понимание java.lang.ClassLoader

Обсудим несколько основных методов класса java.lang.ClassLoader, чтобы получить более четкое представление о том, как он работает.

Метод loadClass()

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

Этот метод отвечает за загрузку класса с заданным параметром имени. Параметр имени относится к полному имени класса.

Виртуальная машина Java вызывает метод loadClass() для разрешения ссылок на классы, задавая для разрешения значение true. Однако не всегда необходимо разрешать класс. Если нужно только определить, существует ли класс или нет, то параметр разрешения устанавливается равным false.

Этот метод служит точкой входа для загрузчика классов.

Можно попытаться понять внутреннюю работу метода loadClass() из исходного кода java.lang.ClassLoader:

protected Class<?> loadClass(String name, boolean resolve)
  throws ClassNotFoundException {
    
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

Реализация метода по умолчанию ищет классы в следующем порядке:

  1. Вызывает метод findLoadedClass(String), чтобы узнать, загружен ли уже класс.
  2. Вызывает метод loadClass(String) в загрузчике родительского класса.
  3. Вызывает метод findClass(String), чтобы найти класс.

Метод defineClass()

protected final Class<?> defineClass(
  String name, byte[] b, int off, int len) throws ClassFormatError

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

Если данные не содержат допустимого класса, выдается ошибка ClassFormatError.

Кроме того, можно переопределить этот метод, так как он помечен как final.

Метод findClass()

protected Class<?> findClass(
  String name) throws ClassNotFoundException

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

Кроме того, loadClass() вызывает этот метод, если загрузчик родительского класса не может найти запрошенный класс.

Реализация по умолчанию генерирует исключение ClassNotFoundException, если ни один из родителей загрузчика класса не находит класс.

Метод getParent()

public final ClassLoader getParent()

Этот метод возвращает загрузчик родительского класса для делегирования.

В некоторых реализациях для представления загрузчика классов начальной загрузки используется значение null.

Метод getResource()

public URL getResource(String name)

Этот метод пытается найти ресурс с заданным именем.

Сначала он делегирует ресурс загрузчику родительского класса. Если родитель имеет значение null, выполняется поиск пути к загрузчику классов, встроенному в виртуальную машину.

Если это не удается, метод вызовет findResource(String) для поиска ресурса. Имя ресурса, указанное в качестве входных данных, может быть относительным или абсолютным по отношению к пути к классам.

Он возвращает объект URL для чтения ресурса или значение null, если ресурс не может быть найден или вызывающий объект не имеет достаточных привилегий для возврата ресурса.

Важно отметить, что Java загружает ресурсы из пути к классам.

Наконец, загрузка ресурсов в Java считается независимой от местоположения, поскольку не имеет значения, где выполняется код, пока среда настроена на поиск ресурсов.

Загрузчики классов контекста (Context Classloaders)

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

Загрузчики классов в JVM следуют иерархической модели, так что каждый загрузчик классов имеет одного родителя, за исключением загрузчика классов начальной загрузки.

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

Например, в JNDI основная функциональность реализуется классами начальной загрузки в rt.jar. Но эти классы JNDI могут загружать поставщиков JNDI, реализованных независимыми поставщиками (развернутыми в пути к классам приложения). Этот сценарий требует, чтобы загрузчик классов начальной загрузки (загрузчик родительских классов) загружал класс, видимый для загрузчика приложений (загрузчик дочерних классов).

Делегирование J2SE здесь не работает, и чтобы обойти эту проблему, нужно найти альтернативные способы загрузки классов. Этого можно добиться с помощью загрузчиков контекста потока.

Класс java.lang.Thread имеет метод getContextClassLoader(), который возвращает ContextClassLoader для конкретного потока. ContextClassLoader предоставляется создателем потока при загрузке ресурсов и классов.

Если значение не установлено, то по умолчанию используется контекст загрузчика классов родительского потока.

Заключение

Загрузчики классов необходимы для выполнения программы Java. В этой статье мы познакомились с ними.

Мы обсудили различные типы загрузчиков классов, а именно загрузчики классов Bootstrap, Extensions и System. Bootstrap служит родителем для всех из них и отвечает за загрузку внутренних классов JDK. С другой стороны, Extensions и System загружают классы из каталога расширений Java и пути к классам соответственно.

Мы также узнали, как работают загрузчики классов и изучили некоторые функции, такие как делегирование, видимость и уникальность. Затем кратко выяснили, как создать собственный загрузчик классов. Наконец, познакомились с загрузчиками класса Context.

Исходный код этих примеров можно найти на GitHub.

Оригинал