Java ClassLoader, пора разобраться в нем досконально

Java

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

Что делает ClassLoader?

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

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

Внутри каждого объекта Class есть поле classLoader, чтобы определить, каким 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.lang.* и т. д. Этот ClassLoader особенный, он реализован кодом C, мы называем его "корневой загрузчик".

ExtensionClassLoader отвечает за загрузку классов расширений JVM, таких как свинг-серия, встроенный js-движок, парсер xml и т. д. Имена этих библиотек обычно начинаются с javax, их пакеты jar находятся в JAVA_HOME/lib/ext/*.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 будет родительское свойство, указывающее на его родительский загрузчик.

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

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

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

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

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

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

Из «Блога ITPUB», ссылка: http://blog.itpub.net/31561269/viewspace-2222522/, если вам необходимо перепечатать, пожалуйста, укажите источник, в противном случае будет преследоваться юридическая ответственность.