Давний Java ClassLoader устарел, если вы его не понимаете

Java JVM
Давний Java ClassLoader устарел, если вы его не понимаете

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

Что делает ClassLoader?

Как следует из названия, он используется для загрузки Class. Он отвечает за преобразование формы байт-кода класса в объект класса в форме памяти. Байт-код может исходить из дискового файла *.class, или *.class в пакете jar, или потока байтов, предоставленного удаленным сервером.Суть байт-кода представляет собой массив байтов []byte, который имеет определенные сложные внутренние формат.

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

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

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, есть много банок Сумка.

AppClassLoader - это погрузчик, непосредственно перед нашими пользователями, он будет загружать пакеты и каталоги 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 будет родительское свойство, указывающее на его родительский загрузчик.

class ClassLoader {
  ...
  private final ClassLoader parent;
  ...
}

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

Class.forName

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

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

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

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 использовать для загрузки.

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

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

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

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

Метод loadClass() является точкой входа для загрузки целевого класса. Сначала он ищет текущий ClassLoader и загружается ли целевой класс в своих родителях. Если он не найден, родители попытаются загрузить его. родители не могут быть загружены, он вызовет findClass(), чтобы позволить пользовательскому загрузчику загрузить сам целевой класс. Метод findClass() класса ClassLoader должен быть переопределен подклассами, и разные загрузчики будут использовать разную логику для получения байт-кода целевого класса. Получив этот байт-код, вызовите метод defineClass(), чтобы преобразовать байт-код в объект класса. Ниже я использую псевдокод для представления основного процесса

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, это означает, что загрузчик родительского класса является «корневым загрузчиком».

// ClassLoader 构造器
protected ClassLoader(String name, ClassLoader parent);

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

Class.forName vs ClassLoader.loadClass

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

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 может решить проблему зависимости от алмаза. Различные версии пакетов используют разные загрузчики классов для загрузки,Классы с одинаковыми именами в разных ClassLoaders на самом деле являются разными классами.. Давайте попробуем простой пример с использованием URLClassLoader, чьим родительским загрузчиком по умолчанию является AppClassLoader.

$ 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));
 }
}

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

$ 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, не могут рассматриваться как один и тот же класс.

depv1Class.equals(depv2Class)

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

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, вы обнаружите, что одно из его полей экземпляра является очень особенным.

class Thread {
  ...
  private ClassLoader contextClassLoader;
  
  public ClassLoader getContextClassLoader() {
    return contextClassLoader;
  }
  
  public void setContextClassLoader(ClassLoader cl) {
    this.contextClassLoader = cl;
  }
  ...
}

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

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

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

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

Во-вторых, contextClassLoader потока по умолчанию наследуется от родительского потока, так называемый родительский поток — это поток, создавший текущий поток. ContextClassLoader основного потока при запуске программы — это AppClassLoader. Это означает, что если нет ручной настройки, то contextClassLoader всех потоков — это AppClassLoader.

Итак, для чего нужен этот contextClassLoader? Мы собираемся объяснить его назначение, используя упомянутые ранее принципы разделения и сотрудничества загрузчиков классов.

Можно совместно использовать классы между потоками, если они используют один и тот же contextClassLoader. ContextClassLoader автоматически передается между родительским и дочерним потоками, поэтому совместное использование будет автоматическим.

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

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

Если мы не настроим contextClassLoader, то все потоки будут использовать AppClassLoader по умолчанию, и все классы будут общими.

ContextClassLoader потоков встречается относительно редко, и если приведенная выше логика неясна, не беспокойтесь слишком сильно.

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