Загрузчики классов в Java

Загрузчики классов в Java

1. Введение в загрузчики классов

Загрузчики классов отвечают заloading Java classes during runtime dynamically to the JVM (виртуальная машина Java). Кроме того, они являются частью JRE (Java Runtime Environment). Следовательно, JVM не нужно знать о лежащих в основе файлах или файловых системах, чтобы запускать программы Java, благодаря загрузчикам классов.

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

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

Дальнейшее чтение:

Понимание утечек памяти в Java

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

Read more

ClassNotFoundException vs NoClassDefFoundError

Узнайте о различиях между ClassNotFoundException и NoClassDefFoundError.

Read more

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

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

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:[email protected]
Class loader of Logging:[email protected]
Class loader of ArrayList:null

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

Загрузчик класса приложения загружает класс, в котором содержится пример метода. An application or system class loader loads our own files in the classpath.с

Затем расширение загружает классLogging. Extension class loaders load classes that are an extension of the standard core Java classes.

Наконец, начальная загрузка загружает классArrayList. A bootstrap or primordial class loader is the parent of all the others.

Однако мы видим, что последний выход дляArrayList отображаетnull на выходе. This is because the bootstrap class loader is written in native code, not Java – so it doesn’t show up as a Java class. По этой причине поведение загрузчика классов начальной загрузки будет отличаться в зависимости от JVM.

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

2.1. Загрузчик классов начальной загрузки

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

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

[.s1] # Он в основном отвечает за загрузку внутренних классов JDK, обычноrt.jar и других основных библиотек, расположенных в$JAVA_HOME/jre/lib directory. ДополнительноBootstrap class loader serves as a parent of all the other ClassLoader instances. #

This bootstrap class loader is part of the core JVM and is written in native code, как указано в приведенном выше примере. На разных платформах могут быть разные реализации этого загрузчика классов.

2.2. Загрузчик класса расширения

extension class loader is a child of the bootstrap class loader and takes care of loading the extensions of the standard core Java classes, чтобы он был доступен для всех приложений, работающих на платформе.

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

2.3. Загрузчик системного класса

С другой стороны, загрузчик классов системы или приложения заботится о загрузке всех классов уровня приложения в JVM. It loads files found in the classpath environment variable, -classpath or -cp command line option. Кроме того, это дочерний элемент загрузчика классов Extensions.

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

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

java.lang.ClassLoader.loadClass() method is responsible for loading the class definition into runtime. Он пытается загрузить класс на основе полного имени.

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

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

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

Давайте посмотрим на пример вывода, когда генерируется ClassNotFoundException.

java.lang.ClassNotFoundException: com.example.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.

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

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

[.s1] # Загрузчики классов следуют модели делегирования, гдеon request to find a class or resource, a ClassLoader instance will delegate the search of the class or resource to the parent class loader. #

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

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

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

[.s1] # Как следствие модели делегирования, легко обеспечитьunique classes as we always try to delegate upwards. #

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

3.3. видимость

[.s1] # Кроме того,children class loaders are visible to classes loaded by its parent class loaders. #

[.s1] # Например, классы, загруженные системным загрузчиком классов, видны в классах, загруженных расширением и загрузчиками классов Bootstrap, но не наоборот. #

[.s1] # Чтобы проиллюстрировать это, если класс A загружен загрузчиком классов приложения, а класс B загружен загрузчиком классов расширений, тогда оба класса A и B видны, поскольку это касается других классов, загруженных загрузчиком классов приложения . #

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

4. Пользовательский ClassLoader

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

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

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

4.1. Сценарии использования пользовательских загрузчиков классов

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

  1. Помощь в модификации существующего байт-кода, например, ткацкие средства

  2. Создание классов, динамически соответствующих потребностям пользователя. e.g in JDBC, switching between different driver implementations is done through dynamic class loading.

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

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

Browsers, for instance, use a custom class loader to load executable content from a website. Браузер может загружать апплеты с разных веб-страниц, используя отдельные загрузчики классов. Средство просмотра апплетов, которое используется для запуска апплетов, содержитClassLoader, который обращается к веб-сайту на удаленном сервере вместо просмотра в локальной файловой системе.

А затем загружает необработанные файлы байт-кода через HTTP и превращает их в классы внутри JVM. Даже если этиapplets have the same name, they are considered as different components if loaded by different class loaders.

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

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

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

Нам нужно расширить класс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;
    }
}

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

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

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

5.1. МетодloadClass()

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

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

Виртуальная машина Java вызывает методloadClass(), чтобы разрешить установку ссылок на классы вtrue. Однако не всегда необходимо разрешать класс. If we only need to determine if the class exists or not, then resolve parameter is set to 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), чтобы найти класс.

5.2. МетодdefineClass()

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

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

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

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

5.3. МетодfindClass()

protected Class findClass(
  String name) throws ClassNotFoundException

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

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

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

5.4. МетодgetParent()

public final ClassLoader getParent()

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

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

5.5. МетодgetResource()

public URL getResource(String name)

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

Сначала он делегирует родительский загрузчик классов для ресурса. If the parent is null, the path of the class loader built into the virtual machine is searched.с

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

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

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

Наконец,resource loading in Java is considered location-independent, поскольку не имеет значения, где выполняется код, если среда настроена на поиск ресурсов.

6. Контекстные загрузчики классов

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

Как мы уже узнали раньше,classloaders in a JVM follow a hierarchical model such that every class loader has a single parent with the exception of the bootstrap class loader.

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

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

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

Классjava.lang.Thread имеет методgetContextClassLoader() that returns the ContextClassLoader for the particular thread. ContextClassLoader предоставляется создателем потока при загрузке ресурсов и классов.

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

7. Заключение

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

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

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

Примеры кода, как всегда, можно найтиover on GitHub.