Загрузчик классов серии JVM (восемнадцать)

JVM

1 Обзор

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

Роль ClassLoader

ClassLoader является основным компонентом Java. Все классы загружаются ClassLoader. ClassLoader отвечает за чтение двоичного потока данных информации о классе в JVM с помощью различных методов и преобразование его в соответствующий целевой класс.java.lang.ClassЗатем экземпляр объекта передается виртуальной машине Java для связывания, инициализации и других операций. Поэтому на всей фазе загрузки ClassLoader может влиять только на загрузку классов, и радует, что через ClassLoader нельзя изменить связывание и инициализацию классов. Что касается того, может ли он работать, это зависит от Execution Engine.

image.pngЗагрузчик классов впервые появился в версии JDK1.0, когда он был разработан только для удовлетворения потребностей приложений Java Applet. Но теперь загрузчики классов сияют в области OSGi и шифрования и дешифрования байт-кода. В основном это связано с тем, что проектировщики виртуальной машины Java не учли ее привязку внутри JVM при проектировании загрузчика классов.Преимущество этого в том, что операция загрузки класса может выполняться более гибко и динамично.

Классификация загрузки классов: явная загрузка против неявной загрузки

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

  • явная загрузкаОтносится к загрузке объекта класса путем вызова ClassLoader в коде, например, непосредственного использованияClass.forName(name)илиthis.getClass().getClassLoader().loadClass()объект класса загрузки
  • Неявная загрузкаЭто не прямой вызов в коде метода ClassLoader для загрузки объекта класса, а автоматическая загрузка его в память через виртуальную машину.Например, при загрузке файла класса определенного класса, файл класса этого класса ссылается на другой объект класса. , дополнительные классы, на которые ссылаются, будут автоматически загружены в память JVM

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

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

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

  • Избегайте встречи в разработкеjava.lang.ClassNotFoundExceptionненормальный илиjava.lang.NoClassDefFoundErrorКогда есть исключение, вы в растерянности.Только поняв механизм загрузки загрузчика, вы сможете быстро найти и решить проблему по журналу ошибок-исключений при возникновении исключения.
  • Когда вам нужно поддерживать динамическую загрузку классов или шифровать и расшифровывать скомпилированные файлы байт-кода, вам нужно иметь дело с загрузчиком классов.
  • Разработчики могут сокращать загрузчики пользовательских классов в программе, чтобы переопределить правила загрузки классов, чтобы реализовать некоторую пользовательскую логику обработки.

Пространства имен

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

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

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

Основные характеристики механизма загрузки классов

  • Модель родительского делегирования. Но не вся загрузка классов следует этой модели.Иногда тип, загружаемый загрузчиком запускаемого класса, может загружать пользовательский код, например внутренний JDK.ServiceProvider/ServiceLoaderМеханизм, пользователи могут предоставить свою собственную реализацию на стандартной платформе API. JDK также должен предоставлять некоторые эталонные реализации по умолчанию, такие как: JNDI, JDBC, файловая система, шифрование и многие другие аспекты Java, все используют этот механизм.В этом случае для загрузки будет использоваться не родительская модель делегирования, а Make использование так называемых контекстных загрузчиков
  • видимость. Загрузчик дочерних классов может обращаться к типам, загруженным родительским загрузчиком, но обратное не допускается, иначе из-за отсутствия необходимой изоляции у нас нет возможности использовать загрузчик классов для реализации логики контейнера
  • единство. Поскольку тип родительского загрузчика виден дочернему загрузчику, тип, загруженный в родительском загрузчике, не будет повторно загружаться в дочернем загрузчике. Но обратите внимание, что один и тот же тип может быть загружен несколько раз между «соседями» загрузчика классов, потому что они не видны друг другу.

2. Классификация загрузчиков классов

JVM поддерживает два типа загрузчиков классов, а именно Bootstrap ClassLoader и определяемый пользователем ClassLoader. Концептуально пользовательский загрузчик классов обычно относится к классу загрузчика классов, настроенному разработчиком в программе, но спецификация виртуальной машины Java не определяет его, но все классы, производные от абстрактного класса ClassLoader Loaders, делятся на пользовательские загрузчики классов. Независимо от того, как разделен тип загрузчика классов, наша общая структура загрузчика классов в программе в основном выглядит следующим образом:

image.png

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

Загрузчик классов Bootstrap (загрузчик классов запуска)

  • Эта загрузка классов реализована на языке C/C++, вложенном в JVM.
  • Он используется для загрузки основной библиотеки Java (JAVA_HOME/jre/lib/rt.jarилиsun.boot.class.pathсодержимое по пути), используемое для предоставления классов, требуемых самой JVM.
  • не наследуется отjava.lang.ClassLoader, нет родительского загрузчика
  • Из соображений безопасности загрузчик классов запуска Bootstrap загружает только пакеты с именамиjava,javax,sunклассы, которые начинаются с
  • Загрузить класс расширения и загрузчик класса приложения и назначить их загрузчиком родительского класса.

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

  • Написано на языке Java авторомsun.misc.Launcher$ExtClassLoaderвыполнить
  • Унаследовано от класса ClassLoader
  • Загрузчик родительского класса является загрузчиком запускаемого класса.
  • отjava.ext.dirsЗагрузите библиотеку классов из каталога, указанного системным свойством, или из каталога установки JDK.jre/lib/extБиблиотека классов загружается в подкаталог.Если созданный пользователем jar помещается в этот каталог, он также будет автоматически загружен загрузчиком классов расширения.

Загрузчик системных классов (загрузчик классов приложений)

  • Написано на языке Java авторомsun.misc.Launcher$AppClassLoaderвыполнить
  • Унаследовано от класса ClassLoader
  • Загрузчик родительского класса является загрузчиком класса расширения.
  • Он отвечает за загрузку переменных среды classpath или системных свойств.java.class.pathБиблиотека классов по указанному пути
  • Загрузчик классов в приложении по умолчанию является системным загрузчиком классов.
  • Это родительский загрузчик по умолчанию для определяемых пользователем загрузчиков классов.
  • через ClassLoadergetSystemClassLoader()способ получить загрузчик классов

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

  • При ежедневной разработке приложений на Java загрузка классов практически выполняется тремя вышеупомянутыми загрузчиками классов в сотрудничестве друг с другом. При необходимости мы также можем настроить загрузчик классов, чтобы настроить способ загрузки классов.
  • Одним из ключевых факторов, отражающих мощную жизнеспособность и большое очарование языка Java, является то, что разработчики Java могут настраивать загрузчик классов для реализации динамической загрузки библиотеки классов.Источником загрузки может быть локальный пакет Jar или удаленный ресурс на сеть.
  • Существует множество реальных вариантов использования, которые можно реализовать с помощью загрузчиков классов в качестве очень удобного механизма подключаемых модулей. Например, упомянутая структура компонентов OSGI и механизм подключаемых модулей Eclipse. Загрузчики классов предоставляют приложениям механизм динамического добавления новых функций без переупаковки и распространения приложения.
  • В то же время пользовательские загрузчики классов могут реализовать изоляцию приложений.Например, промежуточное программное обеспечение и платформы компонентов, такие как Tomcat и Spring, реализуют пользовательские загрузчики внутри и изолируют различные компоненты компонентов с помощью пользовательских загрузчиков. Этот механизм намного совершеннее, чем программы на C/C++.Добавить новые функции в программы на C/C++, не модифицируя их, практически невозможно.Одна лишь совместимость может перекрыть все хорошие идеи.
  • Пользовательские загрузчики классов обычно должны наследоваться от ClassLoader.

ClassLoader о классе массива

Объект Class класса массива не создается загрузчиком классов, а автоматически создается JVM по мере необходимости во время выполнения Java. Для загрузчика класса класса массива это черезClass.getClassLoader()Возвращается так же, как и при загрузчике класса типа элемента в массиве;Если тип элемента в массиве является примитивным типом данных, то класс массива не имеет загрузчика класса.

Проверка кода выглядит следующим образом:

public class ClassLoaderTest2 {
    public static void main(String[] args) {
        String[] arrStr = new String[10];
        System.out.println(arrStr.getClass()); // class [Ljava.lang.String;
        System.out.println(arrStr.getClass().getClassLoader()); // null,表示使用的是引导类加载器

        ClassLoaderTest2[] arr = new ClassLoaderTest2[10];
        System.out.println(arr.getClass()); // class [Lcom.nasuf.jdk8.ClassLoaderTest2;
        System.out.println(arr.getClass().getClassLoader()); // sun.misc.Launcher$AppClassLoader@135fbaa4
        
        int[] arrInt = new int[10];
        System.out.println(arrInt.getClass()); // class [I
        System.out.println(arrInt.getClass().getClassLoader()); // null,基本数据类型,表示不需要类加载器
    }
}

3. Анализ исходного кода ClassLoader

Связь между ClassLoader и существующими загрузчиками классов:

image.png

3.1 Основной метод ClassLoader

Основной метод абстрактного класса ClassLoader (внутри нет абстрактного метода):

  • public final ClassLoader getParent(): возвращает загрузчик родительского класса для этого загрузчика классов.
  • public Class<?> loadClass(String name) throws ClassNotFoundException: загрузить класс с именем name и вернуть результат какjava.lang.Classэкземпляр класса. Возвращает, если класс не найденClassNotFoundExceptionаномальный. Логика этого метода заключается в реализации шаблона родительского делегирования. Этот метод анализируется в разделе 3.1.1 ниже.
  • protected Class<?> findClass(String name) throws ClassNotFoundException: Найдите класс с двоичным именем name и верните результат какjava.lang.Classэкземпляр класса. Это защищенный метод, JVM рекомендует нам переопределить этот метод, требуя, чтобы пользовательский загрузчик следовал механизму родительского делегирования, этот метод будет вызываться после проверки загрузчика родительского класса.loadClass()вызов метода
    • Перед JDK1.2 при загрузке пользовательского класса всегда наследуйте класс ClassLoader и переопределяйте его.loadClassметод для реализации пользовательского загрузчика классов. Однако после JDK1.2 пользователям больше не рекомендуется перезаписыватьloadClass()метод, рекомендуется написать пользовательскую логику загрузки классов вfindClass()метод, то из предыдущего анализа видно, чтоfindClass()метод находится вloadClass()метод вызывается, когдаloadClass()После того, как родительский загрузчик не сможет загрузить метод, он вызовет свой собственныйfindClass()метод для завершения загрузки класса, чтобы убедиться, что пользовательский загрузчик классов также соответствует родительскому механизму делегирования.
    • Следует отметить, что класс ClassLoader не реализуетfindClass()Конкретная логика кода метода вместо броскаClassNotFoundExceptionисключение, в то время как вы должны знать, чтоfindClass()метод обычно иdefineClass()метод, используемый вместе. При нормальных обстоятельствах при настройке загрузчика классов он напрямую переопределяет загрузчик классов.findClass()метод и написать правила загрузки, получить байт-код класса для загрузки, преобразовать его в поток, а затем вызватьdefineClass()метод для создания объекта класса класса
  • protected final Class<?> defineClass(String name, byte[] b, int off, int len): преобразовать заданный байтовый массив b в экземпляр Class.Параметры off и len указывают положение и длину фактической информации о классе в байтовом массиве, где байтовый массив b получен извне с помощью ClassLoader. Это защищенный метод, доступный только в пользовательских подклассах ClassLoader.
    • defineClass()Метод используется для анализа потока байтов в объект класса, который может распознать JVM (логика метода была реализована в ClassLoader).Этот метод может не только создавать экземпляр объекта класса через файл класса, но также создавать экземпляр объекта класса другими способами, например, получение байт-кода класса по сети, а затем преобразование его в поток байтов для создания соответствующего объекта Class
    • defineClass()метод обычно такой же, какfindClass()Этот метод используется вместе.В общем, при настройке загрузчика классов он будет напрямую переопределять метод ClassLoader.findClas()метод и написать правила загрузки, получить байт-код класса для загрузки, преобразовать его в поток, а затем вызватьdefineClass()метод для создания объекта класса класса
    • Кодовый регистр
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }
    
  • protected final void resolveClass(Class<?> c): Класс Java, указанный по ссылке. С помощью этого метода можно одновременно создавать и анализировать объект Class класса. Ранее мы говорили, что этап связывания в основном заключается в проверке байт-кода, выделении памяти для переменной класса и установке начального значения, а также преобразовании символической ссылки в файле байт-кода в прямую ссылку.
  • protected final Class<?> findLoadedClass(String name): Найдите загруженный класс с именем name и верните результат какjava.lang.Classэкземпляр класса. Этот метод является окончательным и не может быть изменен
  • private final ClassLoader parent;: это также экземпляр ClassLoader, и ClassLoader, представленный этим полем, также называется родителем этого ClassLoader. В процессе загрузки класса ClassLoader может передать некоторые запросы своим родителям для обработки.

3.1.1 Анализ метода loadClass

Случай кода:

ClassLoader.getSystemClassLoader().loadClass("com.nasuf.test.User")

Он включает в себя вызов следующих методов, и анализ выглядит следующим образом:

protected Class<?> loadClass(String name, boolean resolve) // resolve: true 表示加载class的同时进行解析操作
    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 {
                    // parent为null:父类加载器为引导类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) { // 当前类的加载器的父类加载器未加载此类 or 当前类的加载器未加载此类
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name); // 调用当前类的ClassLoader的findClass()方法

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {  // 是否进行解析操作
            resolveClass(c);
        }
        return c;
    }
}

3.2 SecurityClassLoader и URLClassLoader

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

Как упоминалось ранее, ClassLoader — это абстрактный класс, и многие методы пусты и не реализованы, например, findClass, findResource и т. д. Класс реализации URLClassLoader предоставляет конкретные реализации для этих методов и добавляет класс URLClassPath, чтобы помочь в получении класса. поток байт-кода и т. д. При написании пользовательского загрузчика классов, если нет чрезмерно сложных требований, вы можете напрямую наследовать класс URLClassLoader, чтобы избежать самостоятельного написания метода findClass и способа получения потока байт-кода, что делает написание пользовательского загрузчика классов более сложным. удобно лаконично

3.3 ExtClassLoader и AppClassLoader

Оба этих класса наследуются от URLClassLoader, статического внутреннего класса sun.misc.Launcher. sun.misc.Launcher в основном используется системой для запуска основного приложения, ExtClassLoader и AppClassLoader создаются sun.misc.Launcher.

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

3.4 Class.forName() и ClassLoader.loadClass()

  • Class.forName(): этостатический метод, чаще всего используетсяClass.forName(String className)Возвращает объект класса на основе полного имени переданного класса. Этот метод одновременно загружает файл класса в память.будет выполнять инициализацию класса
  • ClassLoader.loadClass(): этометод экземпляра, для вызова этого метода требуется объект ClassLoader, который загружает файл класса в память,не выполняет инициализацию класса, который не инициализируется до первого использования класса. Поскольку этот метод должен получить объект ClassLoader, какой загрузчик классов можно указать по мере необходимости, напримерClassLoader cl = ...; cl.loadClass("xxx");

4. Модель родительского делегирования

4.1 Определение и сущность

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

определение

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

Природа

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

image.png

4.2 Преимущества и недостатки механизма родительского делегирования

Преимущество

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

поддержка кода

Механизм родительского делегированияjava.lang.ClassLoader.loadClass(String, boolean)отражается в интерфейсе. Логика этого интерфейса следующая:

  1. Во-первых, проверьте, есть ли целевой класс в кеше текущего загрузчика, и вернитесь напрямую, если он есть.
  2. Определите, является ли родительский загрузчик текущего загрузчика класса пустым, если нет, вызовитеparent.loadClass(name, false)интерфейс для загрузки
  3. И наоборот, если загрузчик родительского класса текущего загрузчика пуст, вызовитеfindBootstrapClassOrNull(name)Интерфейс для загрузки загрузчика классов начальной загрузки
  4. Если указанные выше три пути не загружаются успешно, вызовите интерфейс findClass(name) для загрузки. Этот интерфейс в конечном итоге вызовет собственный интерфейс серии defineClass интерфейса java.lang.ClassLoader для загрузки целевого класса Java.

Модель родительского делегирования скрыта во втором и третьем шагах.

Пример

Предполагая, что класс java.lang.Object в данный момент загружен, очевидно, что этот класс принадлежит базовому классу в JDK, поэтому он должен быть загружен только загрузчиком классов начальной загрузки. Когда JVM будет готова загрузить класс java.lang.Object, JVM будет использовать загрузчик системных классов для его загрузки по умолчанию.Согласно логике загрузки в вышеприведенных 4-х шагах, класс точно не будет найден в Кэш системного класса на первом шаге, и ванная войдет.Шаг второй. Поскольку родительский загрузчик из загрузчика системного класса является загрузчиком класса расширения

считать

Если переопределить в загрузчике классов пользовательские иjava.lang.ClassLoader.loadClass(String)илиjava.lang.ClassLoader.loadClass(String, boolean)метод, сотрите родительский механизм делегирования и сохраните только первый и четвертый шаги из четырех вышеперечисленных шагов, то можно ли загрузить основную библиотеку классов?

Это также невозможно. Поскольку JDK также обеспечивает уровень защиты для основной библиотеки классов, будь то пользовательский загрузчик классов, системный загрузчик классов или загрузчик расширенных классов, в конечном итоге он должен быть вызванjava.lang.defineClass(String, byte[], int, int, ProtectionDomain)метод, который выполняетpreDefineClass()Интерфейс, обеспечивающий защиту основной библиотеки классов JDK.

Недостатки механизма родительского делегирования

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

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

В заключение

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

4.3 Нарушение механизма родительского делегирования

Модель родительского делегирования — это не модель с обязательными ограничениями, а реализация загрузчика классов, рекомендованная дизайнерами Java разработчикам.
Большинство загрузчиков классов в мире Java следуют этой модели. Но есть и исключения: пока не появилась модульность Java, модель родительского делегирования в основном имела 3 масштабные «разрушенные» ситуации.

первый раз

Первый раз фактически произошел до появления модели родительского делегирования — то есть до выхода JDK1.2. Поскольку модель родительского делегирования была введена после JDK1.2, концепция загрузчика классов и абстрактного класса java.lang.ClassLoader уже существовала в первой версии Java, столкнувшись с существующим определяемым пользователем загрузчиком классов, разработчикам Java пришлось пойти на некоторые компромиссы. при введении модели родительского делегирования.Чтобы быть совместимым с этими существующими кодами, больше невозможно избежать возможности перезаписи loadClass() подклассами техническими средствами, только после JDK1.2 Добавить новый защищенный метод findClass( ) в java.lang.ClassLoader и направлять написанную пользователем логику загрузки классов, чтобы максимально переписать этот метод вместо написания кода в loadClass(). В предыдущем разделе мы разобрали метод loadClass().Здесь реализована специфическая логика родительского делегирования.Согласно логике метода loadClass(), если родительский класс не загружается, он автоматически вызывает свой собственный findClass () для завершения загрузки, чтобы он не мешал пользователю загружать классы в соответствии с их собственными пожеланиями, а также мог гарантировать, что вновь написанный класс соответствует правилам родительского делегирования.

второй раз

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

Второе «разрушение» модели родительского делегирования вызвано дефектами самой модели.Родительское делегирование решает проблему согласованности базовых типов при взаимодействии каждого загрузчика классов (более базовые классы заменяются классами более высокого уровня). loader), базовые типы называются «базовыми», потому что они всегда существуют как API, которые наследуются и вызываются пользовательским кодом. Но часто идеальных правил для разработки программ не существует.Если есть базовый тип и ему нужно обращаться к пользовательскому коду, как с ним обращаться?

Это не невозможно.Типичным примером является служба JNDI.JNDI теперь является стандартной службой в Java, и ее код загружается загрузчиком класса запуска (добавлен в rt.jar в JDK1.3), должен быть очень базового типа в Яве. Но цель JNDI состоит в том, чтобы находить ресурсы и централизованно управлять ими. Ему необходимо вызывать код интерфейса поставщика услуг JNDI (SPI) в соответствии с путем к классам приложения, реализованного и развернутого другими производителями. Теперь проблема в том, что это невозможно для загрузчик класса запуска распознает и загрузит эти коды, так что же нам делать? (SPI: на платформе Java интерфейс, который предоставляет внешние службы в базовом классе rt.jar и может быть реализован на прикладном уровне, обычно называется SPI)

Чтобы решить эту проблему, группе разработчиков Java пришлось представить менее элегантную конструкцию: загрузчик класса контекста потока (Thread Context ClassLoader). Этот загрузчик класса можно установить с помощью метода setContextClassLoader() класса java.lang.Thread. Если он все еще установлен при создании потока, он унаследует один из родительского потока, если он не был установлен в глобальном объем приложения Если это так, загрузчик классов по умолчанию является загрузчиком классов приложения.

С загрузчиком класса контекста потока программа может делать некоторые "обманные" вещи. Служба JNDI использует этот загрузчик класса контекста потока для загрузки требуемого кода службы SPI. Это поведение загрузчика родительского класса, чтобы запросить загрузчик дочернего класса завершить загрузку класса. Это поведение фактически открывает родительскую модель делегирования. обратное использование загрузчиков классов, нарушило общие принципы модели родительского делегирования, но и беспомощная вещь. Загрузка SPI, разработанных на Java, в основном выполняется таким образом, например, JNDI, JDBC, JCE, JAXB и JBI и т. д. Однако, когда имеется более одного поставщика услуг SPI, код может основываться только на конкретных поставщиках. Чтобы устранить эту чрезвычайно неэлегантную реализацию, в JDK6 предоставляется класс java.util.ServiceLoader, который использует информацию о конфигурации в META-INFO/services и режиме цепочки ответственности, что обеспечивает относительно разумное решение для загрузки SPI. Загрузчик контекста по умолчанию — это загрузчик классов приложения, который использует загрузчик контекста в качестве посредника, чтобы код в загрузчике классов запуска также мог обращаться к классам в загрузчике классов приложения.

в третий раз

Третье «разрушение» родительской модели делегирования вызвано погоней пользователя за программной динамикой, такой как: горячая замена кода (Hot Swap), горячее развертывание модуля (Hot Deployment) и т.д.

Ключом к реализации модульного горячего развертывания в JSR-291 (т. е. OSGI R4.2) под руководством IBM является реализация собственного механизма загрузчика классов, каждый программный модуль (называемый Bundle в OSGI) имеет свой собственный загрузчик классов, когда требуется Bundle. для замены Bundle возвращается вместе с загрузчиком классов для обеспечения горячей замены кода. В среде OSGI загрузчик классов больше не является древовидной структурой, рекомендованной родительской моделью делегирования, а получил дальнейшее развитие в более сложную сетчатую структуру.

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

  1. Делегировать классы, начинающиеся с java.*, в загрузчик родительских классов для загрузки
  2. В противном случае класс в списке делегирования будет делегирован загрузчику родительского класса для загрузки.
  3. В противном случае делегируйте классы в списке Import загрузчику классов Bundle класса Export.
  4. В противном случае найдите ClassPath текущего пакета и используйте его собственный загрузчик классов для загрузки.
  5. В противном случае найдите, находится ли класс в своем собственном пакете фрагментов, и если да, делегируйте загрузчику классов пакета фрагментов для загрузки.
  6. В противном случае найдите пакет в списке динамического импорта и делегируйте загрузчику классов соответствующий пакет для загрузки.
  7. В противном случае поиск класса завершится ошибкой.

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

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

4.4 Внедрение горячей замены

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

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

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

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

image.png

Случай кода:

package com.nasuf.jdk8;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

// 自定义类加载器
public class MyClassLoader extends ClassLoader {

    private String rootDir;

    public MyClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }
    
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        Class clazz = this.findLoadedClass(className);
        FileChannel fileChannel = null;
        WritableByteChannel outChannel = null;
        if (null == clazz) {
            try {
                String classFile = getClassFile(className);
                FileInputStream fis = new FileInputStream(classFile);
                fileChannel = fis.getChannel();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                outChannel = Channels.newChannel(baos);
                ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
                while (true) {
                    int i = fileChannel.read(buffer);
                    if (i == 0 || i == -1) {
                        break;
                    }
                    buffer.flip();
                    outChannel.write(buffer);
                    buffer.clear();
                }
                byte[] bytes = baos.toByteArray();
                clazz = defineClass(className, bytes, 0, bytes.length);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (fileChannel != null)
                        fileChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    if (outChannel != null)
                        outChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return clazz;
    }

    private String getClassFile(String className) {
        return rootDir + "/" + className.replace('.', '/') + ".class";
    }
}
package com.nasuf.jdk8;

public class Demo1 {

    public void hot() {
        System.out.println("OldDemo1");
    }
}
package com.nasuf.jdk8;

import java.lang.reflect.Method;

public class LoopRun {
    public static void main(String[] args) {
        while(true) {
            try {
                // 创建自定义类加载器的实例
                MyClassLoader loader
                        = new MyClassLoader("/Users/nasuf/Project/JvmNotesCode/jdk1.8-module/src/");
                // 加载指定的类
                Class<?> clazz = loader.findClass("com.nasuf.jdk8.Demo1");
                // 创建运行时类的实例
                Object demo = clazz.newInstance();
                // 获取运行时类中指定的方法
                Method m = clazz.getMethod("hot");
                // 调用指定的方法
                m.invoke(demo);
                Thread.sleep(5000);
            } catch (Exception e) {
                System.out.println("Not Found");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

Запустите основной метод вышеприведенного LoopRun и выводите следующее каждые 5 секунд:

OldDemo1
OldDemo1
OldDemo1
OldDemo1
OldDemo1

Не останавливайте выполнение программы в это время, измените оператор вывода класса Demo1 и скомпилируйте его.

package com.nasuf.jdk8;

public class Demo1 {

    public void hot() {
        System.out.println("NewDemo1");
    }
}

На данный момент вывод консоли выглядит следующим образом:

OldDemo1
OldDemo1
OldDemo1
NewDemo1
NewDemo1
NewDemo1

Вступает в силу немедленно.

5. Механизм безопасности песочницы

  • Обеспечение безопасности программы
  • Защитите собственный код JDK Java

Ядром модели безопасности Java является песочница Java. Так называемая «песочница» — это среда, в которой запуск программ ограничен.

Механизм песочницы предназначен для ограничения Java-кода конкретной рабочей областью виртуальной машины и строгого ограничения доступа к локальным системным ресурсам кучи кода. Такие меры используются для обеспечения ограниченной изоляции кода и предотвращения повреждения локальной системы.

Песочница в основном ограничивает доступ к системным ресурсам, включая ЦП, память, файловую систему и сеть. Разные уровни песочниц также могут иметь разные ограничения на доступ к этим ресурсам. Все Java-программы можно запускать в специальной изолированной программной среде, а политики безопасности можно настраивать

5.1 Период JDK 1.0

В Java исполнительная программа делится на два типа: локальный код и удаленный код. Локальный код считается надежным по умолчанию, а удаленный код считается ненадежным. Для доверенного собственного кода можно получить доступ ко всем собственным ресурсам. Безопасность ненадежного удаленного кода в ранних реализациях Java основывалась на механизмах песочницы. Модель безопасности JDK1.0 показана на следующем рисунке.

image.png

5.2 JDK 1.1 период

Такой строгий механизм безопасности в JDK 1.0 также создает препятствия для расширения функций программы, например, когда пользователь хочет, чтобы удаленный код получил доступ к файлам локальной системы, это невозможно реализовать. Поэтому в последующей версии JDK 1.1 были внесены улучшения в механизм безопасности и добавлены политики безопасности. Позволяет пользователям указывать кодовый доступ к локальным ресурсам, как показано на следующем рисунке:

image.png

5.3 Период JDK 1.2

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

image.png

5.4 JDK 1.6 период

Реализация новейшего механизма безопасности вводит понятие домена.

Виртуальная машина загружает весь код в разные системные домены и домены приложений. Часть системного домена специально отвечает за взаимодействие с ключевыми ресурсами, и каждая часть домена приложения получает доступ к различным требуемым ресурсам через некоторых агентов системного домена. Разные защищенные домены (Protected Domains) в виртуальной машине соответствуют разным разрешениям. Файлы классов, существующие в разных доменах, имеют полные права доступа текущего домена. Как показано ниже:

image.png

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

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

  • классы нагрузки изолированно

В некоторых фреймворках ПО промежуточного слоя изолировано от модулей приложения, а классы загружаются в разные среды. Например, платформа контейнеров в Alibaba использует пользовательский загрузчик классов, чтобы гарантировать, что пакет jar, от которого зависит приложение, не повлияет на пакет jar, используемый при работе промежуточного программного обеспечения. Другой пример: серверы веб-приложений, такие как Tomcat, имеют несколько внутренних настраиваемых загрузчиков классов для изоляции различных приложений на одном сервере веб-приложений.

  • Изменить способ загрузки классов

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

  • источник загрузки расширения

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

  • Предотвратить утечку исходного кода

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

общие сценарии

  • Достигая аналогичной изоляции внутри процесса, загрузчики классов фактически используются как разные пространства имен, чтобы обеспечить контейнероподобный модульный эффект. Например, два модуля, которые зависят от разных версий библиотеки классов, могут загружаться из разных контейнеров, не мешая друг другу. Преемниками в этой области являются Java EE и такие фреймворки, как OSGI и JPMS.
  • Приложения должны получать информацию об определении класса из другого источника данных, например из сетевого источника данных, а не из локальной файловой системы. Или вам нужно самостоятельно манипулировать байт-кодом, динамически изменять или генерировать типы

Уведомление

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

6.1 Реализация

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

Метод реализации

  • Java предоставляет абстрактные классыjava.lang.ClassLoader, все определяемые пользователем загрузчики классов должны наследовать класс ClassLoader.
  • При настройке подкласса ClassLoader у нас обычно есть два подхода:
    • Способ 1: переписатьloadClass()метод
    • Способ 2: переписатьfindClass()метод

В сравнении

Эти два метода похожи по своей сути, ведь loadClass() также будет вызывать метод findClass(), но, по логике вещей, лучше не изменять логику внутри loadClass() напрямую.Рекомендуемый подход состоит в том, чтобы только переписать метод загрузки пользовательского класса в findClass(), указать имя класса в соответствии с параметром и вернуть ссылку на соответствующий объект класса.

  • В методе loadClass() реализована логика родительской модели делегирования. Изменение этого метода без авторизации приведет к разрушению модели, что, вероятно, вызовет проблемы. Поэтому лучше всего вносить небольшие изменения в рамках родительской модели делегирования, не нарушая исходную стабильную структуру. В то же время это также позволяет избежать написания повторяющегося кода, делегированного родителями, в процессе переписывания метода loadClass().С точки зрения повторного использования кода всегда лучше не изменять этот метод напрямую.
  • После написания пользовательского загрузчика классов вы можете вызвать в программе метод loadClass() для реализации операции загрузки классов.

иллюстрировать

  • Загрузчик родительского класса пользовательского загрузчика классов является системным загрузчиком классов.
  • Вся загрузка классов в JVM используетjava.lang.ClassLoader.laodClass(String)Интерфейс (за исключением пользовательского загрузчика классов и переписывания интерфейса), даже основная библиотека классов JDK не является исключением

7. Новые функции в Java 9

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

  • Механизм расширения был удален, а загрузчик класса расширения был сохранен по соображениям обратной совместимости, но переименован в загрузчик класса платформы. Новые методы, доступные через ClassLoadergetPlatformClassLoader()чтобы получить. JDK 9 построен на основе модульности (исходный rt.jar и tools.jar разбит на десятки JMOD-файлов), а библиотека классов Java в нем, естественно, отвечает требованиям расширяемости, поэтому нет необходимости ее хранить.<JAVA_HOME>\lib\extкаталог, который ранее использовал этот каталог илиjava.ext.dirsСистемные переменные для расширения функциональности JDK больше не имеют значения.
  • И загрузчик класса платформы, и загрузчик класса приложения больше не наследуются отjava.net.URLClassLoader. Теперь загрузчик классов запуска, загрузчик классов платформы и загрузчик классов приложений наследуются отjdk.internal.loader.BuiltinClassLoader

image.png

Если есть программа, которая напрямую зависит от этого отношения наследования или зависит от конкретного метода класса URLClassLoader, код, скорее всего, даст сбой в JDK9 и более поздних версиях JDK.

  • В JDK9 загрузчики классов имеют имена, которые указываются в конструкторе и могут быть доступны черезgetName()способ получения. Имя загрузчика класса платформыplatform, имя загрузчика класса приложенияapp. Имя загрузчика классов может быть полезно при отладке проблем, связанных с загрузчиком классов.
  • Загрузчик классов запуска теперь представляет собой загрузчик классов, реализованный в сотрудничестве с библиотекой классов java внутри jvm (ранее реализованный на C++), но для совместимости с предыдущим кодом он по-прежнему будет возвращать значение null в сценарии получения загрузчика классов запуска, вместо получит экземпляр BootstrapClassLoader

проверка кода

public class ClassLoaderTest {
    public static void main(String[] args) {
        System.out.println(ClassLoaderTest.class.getClassLoader());
        System.out.println(ClassLoaderTest.class.getClassLoader().getParent());
        System.out.println(ClassLoaderTest.class.getClassLoader().getParent().getParent());
    }
}

Приведенный выше код находится вJDK8При выполнении в среде вывод выглядит следующим образом:

sun.misc.Launcher$AppClassLoader@135fbaa4
sun.misc.Launcher$ExtClassLoader@2503dbd3
null

пока вJDK9При выполнении в среде вывод выглядит следующим образом:

jdk.internal.loader.ClassLoaders$AppClassLoader@4f8e5cde
jdk.internal.loader.ClassLoaders$PlatformClassLoader@311d617d
null
  • Также изменилось делегирование загрузки классов. Когда загрузчик класса платформы и приложения получает запрос на загрузку класса, прежде чем делегировать загрузчик родительского класса для загрузки, он должен сначала определить, может ли класс принадлежать определенному системному модулю.Делегировать загрузчику, ответственному за этот модуль, для завершения загрузки

image.png

image.png