Что такое ClassLoader

Java

Что делает ClassLoader?

Как следует из названия, он используется для загрузки Class. Он отвечает за преобразование формы байт-кода класса в объект класса в форме памяти. Байт-код может исходить из дискового файла *.class, или *.class в пакете jar, или потока байтов, предоставленного удаленным сервером.Суть байт-кода представляет собой массив байтов []byte, который имеет определенные сложные внутренние формат.
Существует множество методов шифрования байт-кода, реализация которых зависит от пользовательских загрузчиков классов. Сначала используйте инструмент для шифрования файла байт-кода, а затем используйте настраиваемый ClassLoader для расшифровки содержимого файла во время выполнения, а затем загрузите расшифрованный байт-код. Внутри каждого объекта Class есть поле classLoader, чтобы определить, каким ClassLoader он был загружен. ClassLoader подобен контейнеру, который содержит много загруженных объектов класса.?
1 2 3 4 5 class Class<T> {   ...   private final ClassLoader classLoader;   ... }

ленивая загрузка

Операция JVM не загружает все требуемые классы за один раз, она загружается по требованию, то есть ленивая загрузка. В процессе работы программы она будет постепенно сталкиваться со многими новыми классами, которых она не знает, и в это время будет вызываться ClassLoader для загрузки этих классов. После завершения загрузки объект Class будет сохранен в ClassLoader, и его не нужно будет перезагружать в следующий раз. Например, когда вы вызываете статический метод класса, класс должен быть загружен первым, но поле экземпляра класса не будет затронуто, поэтому класс класса поля экземпляра не нужно загружать временно, но он может загружать классы, связанные со статическими полями, поскольку статические методы обращаются к статическим полям. Класс полей экземпляра не может быть загружен, пока вы не создадите экземпляр объекта.

выполнять свои обязанности

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

В JVM встроены три важных ClassLoader, а именно BootstrapClassLoader, ExtensionClassLoader и AppClassLoader.

BootstrapClassLoader отвечает за загрузку основных классов среды выполнения JVM, эти классы находятся в файле $JAVA_HOME/lib/rt.jar, обычно мы используем встроенные библиотеки.java.xxx.* Это все там, как java.util.,java.io., java.nio., java.яз.и т.п. Этот ClassLoader особенный, он реализован кодом C, мы называем его "корневой загрузчик". ExtensionClassLoader отвечает за загрузку классов расширений JVM, таких как серия свинга, встроенный движок js, xml Парсеры и т. д. Названия этих библиотек обычно начинаются с javax, их пакеты jar находятся в $JAVA_HOME/lib/ext/*.jar, существует множество пакетов jar. AppClassLoader — это загрузчик, непосредственно обращенный к нашим пользователям, он будет загружать пакеты и каталоги jar по пути, указанному в переменной среды Classpath. Код, который мы пишем, и сторонние jar-пакеты, которые мы используем, обычно загружаются им. Для тех пакетов jar и файлов классов, предоставляемых статическими файловыми серверами в сети, в jdk есть встроенный URLClassLoader.Пользователям нужно только передать канонический сетевой путь конструктору для использования URLClassLoader для загрузки удаленной библиотеки классов. URLClassLoader может не только загружать библиотеку удаленных классов, но и загружать библиотеку классов локального пути, в зависимости от различных форм адреса в конструкторе. И ExtensionClassLoader, и AppClassLoader являются подклассами URLClassLoader, и оба они загружают библиотеки классов из локальной файловой системы.
AppClassLoader можно получить с помощью статического метода getSystemClassLoader(), предоставляемого классом ClassLoader.Это то, что мы называем «системным загрузчиком классов», и код класса, который обычно пишут наши пользователи, обычно загружается им. когда наш При выполнении основного метода загрузчиком для первого пользовательского класса является AppClassLoader.

ClassLoader переходный

В процессе работы программа сталкивается с неизвестным классом, какой ClassLoader она выберет для его загрузки? Стратегия виртуальной машины заключается в использовании ClassLoader объекта Class вызывающего объекта для загрузки неизвестных в настоящее время классов. Что такое вызывающий объект класса? То есть при встрече с этим неизвестным классом виртуальная машина должна выполнять вызов метода (статический метод или метод экземпляра), на каком классе висит метод, тогда этот класс является вызывающим объектом Class. Ранее мы упоминали, что у каждого объекта Class есть свойство classLoader, которое записывает, кто загрузил текущий класс. Из-за транзитивности ClassLoader все классы с ленивой загрузкой будут нести полную ответственность за ClassLoader, который первоначально вызывает основной метод, которым является AppClassLoader.

родительская делегация

Ранее мы упоминали, что AppClassLoader отвечает только за загрузку библиотек классов в пути к классам. Что, если он обнаружит незагруженную библиотеку системных классов? AppClassLoader должен передать загрузку библиотеки системных классов BootstrapClassLoader и ExtensionClassLoader. Это то, что мы часто говорим " Назначение родителей».

Когда AppClassLoader загружает неизвестное имя класса, он не сразу ищет путь к классам. Он сначала передает имя класса для загрузки ExtensionClassLoader. Если ExtensionClassLoader может быть загружен, то AppClassLoader не придется об этом беспокоиться. В противном случае он ищет путь к классам. Когда ExtensionClassLoader загружает неизвестное имя класса, он не сразу ищет путь ext, а сначала передает имя класса BootstrapClassLoader. Чтобы загрузить, если BootstrapClassLoader может быть загружен, то ExtensionClassLoader не должен беспокоить. В противном случае он будет искать банки в пути ext.
Три загрузчика классов образуют каскадные отношения отца и сына. Каждый загрузчик классов ленив и пытается оставить работу своему отцу. Если отец не может этого сделать, он сделает это сам. Внутри каждого объекта ClassLoader будет родительское свойство, указывающее на его родительский загрузчик.?
1 2 3 4 5 class ClassLoader {   ...   private final ClassLoader parent;   ... }

Стоит отметить, что родительский указатель ExtensionClassLoader на рисунке нарисован пунктирной линией, это потому, что значение его родителя равно null, когда родительское поле равно null, это означает, что его родительский загрузчик является «корневым загрузчиком». . Если значение атрибута classLoader объекта Class равно null, это означает, что этот класс также загружается «корневым загрузчиком». Обратите внимание, что родитель здесь не супер или родительский класс, а просто поле внутри ClassLoader.

Class.forName

Когда мы используем драйвер jdbc, мы часто используем метод Class.forName для динамической загрузки класса драйвера.

?
1 Class.forName("com.mysql.cj.jdbc.Driver");

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

?
1 2 3 4 5 6 7 8 9 10 class Driver {   static {     try {        java.sql.DriverManager.registerDriver( new Driver());     } catch (SQLException E) {        throw new RuntimeException("Can't register driver!");     }   }   ... }

Метод forName также использует ClassLoader объекта Class вызывающего объекта для загрузки целевого класса. Однако forName также предоставляет версию с несколькими параметрами, которая может указывать, какой ClassLoader использовать для загрузки.

?
1 Class<?> forName(String name, boolean initialize, ClassLoader cl)

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

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

В ClassLoader есть три важных метода: loadClass(), findClass() и defineClass().

Метод loadClass() является точкой входа для загрузки целевого класса. Сначала он ищет текущий ClassLoader и загружается ли целевой класс в своих родителях. Если он не найден, родители попытаются загрузить его. родители не могут быть загружены, он вызовет findClass(), чтобы позволить пользовательскому загрузчику загрузить сам целевой класс. Метод findClass() класса ClassLoader должен быть переопределен подклассами, и разные загрузчики будут использовать разную логику для получения байт-кода целевого класса. Получив этот байт-код, вызовите Метод defineClass() преобразует байт-код в объект класса. Ниже я использую псевдокод для представления основного процесса?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class ClassLoader {     // 加载入口,定义了双亲委派规则   Class loadClass(String name) {     // 是否已经加载了     Class t = this .findFromLoaded(name);     if(t == null ) {       // 交给双亲       t = this .parent.loadClass(name)     }     if(t == null ) {       // 双亲都不行,只能靠自己了       t = this .findClass(name);     }     return t;   }       // 交给子类自己去实现   Class findClass(String name) {     throw ClassNotFoundException();   }       // 组装Class对象   Class defineClass(byte [] code, String name) {     return buildClassFromCode(code, name);   } }   class CustomClassLoader extends ClassLoader {     Class findClass(String name) {     // 寻找字节码     byte[] code = findCodeFromSomewhere(name);     // 组装Class对象     return this .defineClass(code, name);   } }
Пользовательский загрузчик классов не так просто разрушить правило делегирования родителей, не легко покрыть метод loadClass. Это может привести к тому, что загрузчик не сможет загрузить пользовательские встроенные основные библиотеки. При использовании пользовательского загрузчика полезно очистить его родительский загрузчик, создав подкласс входящего. Если загрузчик родительского класса имеет значение null, это означает, что родительская загрузка является «корнем загрузчика».?
1 2 // ClassLoader 构造器 protected ClassLoader(String name, ClassLoader parent);

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

Class.forName vs ClassLoader.loadClass

Оба этих метода можно использовать для загрузки целевого класса, между ними есть небольшая разница, то есть метод Class.forName() может получить класс нативного типа, а ClassLoader.loadClass() сообщит об ошибке .

?
1 2 3 4 5 6 7 8 9 10 11 Class<?> x = Class.forName("[I"); System.out.println(x);   x = ClassLoader.getSystemClassLoader().loadClass("[I"); System.out.println(x);   --------------------- class [I   Exception in thread "main" java.lang.ClassNotFoundException: [I ...

Алмазная зависимость

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

Maven, который мы обычно используем, решает алмазные зависимости таким образом.Он выбирает одну из нескольких конфликтующих версий для использования.Если совместимость между разными версиями очень плохая, то программа не будет нормально компилироваться и работать. Эта форма Maven называется «уплощенным» управлением зависимостями.

Использование ClassLoader может решить проблему зависимости от алмаза. Различные версии пакетов используют разные загрузчики классов для загрузки,Классы с тем же именем в разных классах на самом деле разные классы. Давайте попробуем простой пример с использованием URLClassLoader, родительский загрузчик которого по умолчанию AppClassLoader
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 $ cat ~/source/jcl/v1/Dep.java public class Dep {     public void print() {         System.out.println( "v1" );     } }   $ cat ~/source/jcl/v2/Dep.java public class Dep {  public void print() {   System.out.println("v1" );  } }   $ cat ~/source/jcl/Test.java public class Test {     public static void main(String[] args) throws Exception {         String v1dir = "file:///Users/qianwp/source/jcl/v1/" ;         String v2dir = "file:///Users/qianwp/source/jcl/v2/" ;         URLClassLoader v1 = new URLClassLoader( new URL[]{ new URL(v1dir)});         URLClassLoader v2 = new URLClassLoader( new URL[]{ new URL(v2dir)});             Class<?> depv1Class = v1.loadClass("Dep" );         Object depv1 = depv1Class.getConstructor().newInstance();         depv1Class.getMethod( "print").invoke(depv1);           Class<?> depv2Class = v2.loadClass( "Dep");         Object depv2 = depv2Class.getConstructor().newInstance();         depv2Class.getMethod( "print").invoke(depv2);          System.out.println(depv1Class.equals(depv2Class));  } }

Перед запуском нам нужно скомпилировать зависимую библиотеку классов

?
1 2 3 4 5 6 7 8 9 10 $ cd ~/source/jcl/v1 $ javac Dep.java $ cd ~/source/jcl/v2 $ javac Dep.java $ cd ~/source/jcl $ javac Test.java $ java Test v1 v2 false

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

?
1 depv1Class.equals(depv2Class)

Мы также можем сделать так, чтобы две разные версии класса Dep реализовывали один и тот же интерфейс, что позволяет избежать использования отражения для вызова методов класса Dep.

?
1 2 3 Class<?> depv1Class = v1.loadClass("Dep"); IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance(); depv1.print()
Хотя ClassLoader может решить проблему конфликтов зависимостей, он также ограничивает рабочий интерфейс различных программных пакетов для динамического вызова посредством отражения или интерфейса. Maven не имеет этого ограничения.Он основан на стратегии ленивой загрузки виртуальной машины по умолчанию.Если пользовательский ClassLoader не отображается во время запущенного процесса, AppClassLoader используется от начала до конца, и должны быть разные версии одного и того же класса. загружены с использованием разных ClassLoaders. , поэтому Maven не может идеально решить алмазные зависимости. Если вы хотите узнать, существует ли инструмент управления пакетами с открытым исходным кодом, который может решить алмазные зависимости, я рекомендую вам узнать о диван-арк, который представляет собой облегченную инфраструктуру изоляции с открытым исходным кодом для Ant Financial.

Разделение труда и сотрудничество

Здесь мы заново понимаем значение ClassLoader, которое эквивалентно пространству имен класса и играет роль изоляции класса. Имена классов в одном и том же ClassLoader уникальны, и разные ClassLoaders могут содержать классы с одинаковыми именами. ClassLoader — это контейнер для имен классов, песочница для классов.

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

Thread.contextClassLoader

Если вы немного почитаете исходный код Thread, вы обнаружите, что одно из его полей экземпляра является очень особенным.?
1 2 3 4 5 6 7 8 9 10 11 12 13 class Thread {   ...   private ClassLoader contextClassLoader;       public ClassLoader getContextClassLoader() {     return contextClassLoader;   }       public void setContextClassLoader(ClassLoader cl) {     this.contextClassLoader = cl;   }   ... }

contextClassLoader «загрузчик класса контекста потока», что это такое?

?
1 Thread.currentThread().getContextClassLoader().loadClass(name);

Это означает, что если вы загружаете целевой класс с помощью метода forName(string name), он не будет автоматически использовать contextClassLoader. Классы, загруженные лениво из-за зависимостей кода, также не загружаются автоматически с помощью contextClassLoader.

Во-вторых, contextClassLoader потока по умолчанию наследуется от родительского потока, так называемый родительский поток — это поток, создавший текущий поток. ContextClassLoader основного потока при запуске программы — это AppClassLoader. Это означает, что если нет ручной настройки, все потоки contextClassLoader одновременно является AppClassLoader. Итак, для чего нужен этот contextClassLoader? Мы собираемся объяснить его назначение, используя упомянутые ранее принципы разделения и сотрудничества загрузчиков классов. Он может совместно использовать классы между потоками, если они используют один и тот же contextClassLoader. ContextClassLoader автоматически передается между родительским и дочерним потоками, поэтому совместное использование будет автоматическим. Если разные потоки используют разные загрузчики contextClassLoaders, классы, используемые разными потоками, могут быть изолированы. Если мы разделим бизнес, разные предприятия используют разные пулы потоков, совместно используют один и тот же contextClassLoader внутри пула потоков и используют разные contextClassLoaders между пулами потоков, что может сыграть хорошую роль в защите изоляции и избежать конфликтов версий классов. Если мы не настроим contextClassLoader, то все потоки будут использовать AppClassLoader по умолчанию, и все классы будут общими. ContextClassLoader потока редко используется, и если приведенная выше логика неясна, не беспокойтесь слишком сильно. После того, как JDK9 добавил функцию модуля, структура загрузчика классов была изменена в определенной степени, но принцип загрузчика классов все еще аналогичен.Как контейнер класса, он играет роль изоляции класса, и ему также необходимо полагаться на родительский механизм делегирования, чтобы установить отношения сотрудничества между различными загрузчиками классов. 
Ссылка на перепечатку: https://juejin.cn/post/6844903729435508750