Подробно объясните механизм загрузки классов Java.

Java
Подробно объясните механизм загрузки классов Java.

Один: ClassLoader

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

Структура JVM HotSpot, изображение из Java Garbage Collection Basics

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

В Java процесс загрузки класса примерно разделен на следующие этапы:

  1. Получить массив байтов файла класса по полному имени класса. Он может исходить из локальных файлов, пакетов jar, сети и т. д.
  2. Информация описания и статические атрибуты класса сохраняются в области методов/метапространстве.
  3. Создайте соответствующий объект java.lang.Class в куче JVM.

Понимание механизма загрузки классов в Java очень помогает в понимании JVM.

Второй: загрузчик классов Java по умолчанию

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

  • Bootstrap ClassLoader
  • Extension ClassLoader
  • App ClassLoader

Bootstrap ClassLoaderОтвечает за загрузку фундаментального класса Java, в основном %jre_home%/lib/directory RT.jar, resources.jar, charsets.jar и т.д.

Extension ClassLoaderОтвечает за загрузку классов расширений Java, в основном jar-файлов в каталоге %JRE_HOME%/lib/ext.

App ClassLoaderОтвечает за загрузку всех классов в применяемом в настоящее время ClassPath.

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

public class ClassPath {
    public static void main(String[] args) {
        System.out.println("Bootstrap ClassLoader path: ");
        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println("----------------------------");

        System.out.println("Extension ClassLoader path: ");
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println("----------------------------");

        System.out.println("App ClassLoader path: ");
        System.out.println(System.getProperty("java.class.path"));
        System.out.println("----------------------------");
    }
}

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

Bootstrap ClassLoader находится на уровне JVM и написан на C++.

И Extension ClassLoader, и App ClassLoader являются классами Java.

JVM запускает Bootstrap ClassLoader, а затем инициализирует sun.misc.Launcher.

Затем Launcher инициализирует Extension ClassLoader и App ClassLoader.

Третье: анализ исходного кода

Класс sun.misc.Launcher — это точка входа в Java-программу.

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

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    Thread.currentThread().setContextClassLoader(this.loader);
    ……
}

Есть еще две важные строки кода:

Launcher.ExtClassLoader.getExtClassLoader();

this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

Первая строка инициализирует ExtClassLoader, но не указывает его родителя.

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

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

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

Конкретную логику можно просмотреть в исходном коде в следующем порядке:

  1. Метод getClassLoader() класса Launcher.
  2. Метод initSystemClassLoader() класса ClassLoader.
  3. Метод getSystemClassLoader() класса ClassLoader.
  4. Способ классы () класса классов класса.

Аннотация метода getSystemClassLoader():

/**
* Returns the system class loader for delegation. This is the default
* delegation parent for new <tt>ClassLoader</tt> instances, and is
* typically the class loader used to start the application.
**/


И ExtClassLoader, и AppClassLoader наследуют класс URLClassLoader.

URLClassLoader поддерживает загрузку классов из каталогов файлов и пакетов jar.

И ExtClassLoader, и AppClassLoader вызывают конструктор родительского класса.

public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory)

В классе URLClassLoader есть свойство какucp, указывающий путь, за поиск которого отвечает ClassLoader.

Самая большая разница между ExtClassLoader и AppClassLoader заключается в том, что они отвечают за разные пути.

/* The search path for classes and resources */
private final URLClassPath ucp;

Просмотрите исходный код, чтобы получить:

Путь, за поиск которого отвечает ExtClassLoader:

String var0 = System.getProperty("java.ext.dirs");

Путь, за поиск которого отвечает AppClassLoader:

String var1 = System.getProperty("java.class.path");

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

Кроме того, путь поиска классного загрузчика Bootstrap отвечает за:

String bootClassPath = System.getProperty("sun.boot.class.path");

Исходный код ClassLoader

ClassLoader — это абстрактный класс с несколькими основными методами:

  • defineClass(String name, byte[] b, int off, int len) преобразует содержимое массива байтов b в класс Java, а возвращаемый результат является экземпляром класса java.lang.Class.

  • findClass(String name) находит класс с именем name, а возвращаемый результат является экземпляром класса java.lang.Class.

  • loadClass(String name) загружает класс с именем name, а возвращаемый результат является экземпляром класса java.lang.Class.

  • resolveClass(Class> c) ​​связывает указанный класс Java.

Среди них метод LoadClass является одним из наиболее часто вовлеченных.

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

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // 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;
    }
}

Основные этапы этого метода следующие:

  1. Укажите полное имя класса для загрузки, сначала вызовите findLoadedClass(name), чтобы определить, загрузил ли текущий класс загрузчик.
  2. если не загружен. Затем оцените, является ли родительский загрузчик текущего ClassLoader нулевым. Если не нуль, его родительский загрузчик делегируется для загрузки. Если null, используйте Bootstrap ClassLoader для загрузки.
  3. Если ни родительский загрузчик, ни Bootstrap ClassLoader не могут быть загружены, вызывается метод findClass(name) для поиска класса, который необходимо загрузить.

Кроме того, метод loadClass также включает в себя процесс блокировки с использованием ConcurrentHashMap для блокировки различных полных имен классов.

Подробнее см. Метод GetClassLoadingLocklocklock.

Четыре: режим родительского доверия

Механизм загрузки классов Java использует шаблон родительского делегирования.

Когда ClassLoader загружает класс, ему сначала нужно делегировать задачу своему родительскому загрузчику, вплоть до Bootstrap ClassLoader.

Если родительский загрузчик не загружает класс, он будет возвращаться делегированному инициатору, то есть текущему ClassLoader, слой за слоем для загрузки.

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

Работа по загрузке классов сначала инициируется App ClassLoader, затем делегируется Extension ClassLoader и, наконец, делегируется Bootstrap ClassLoader.


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

Создайте новый пакет jar с именем acai-cl.jar с простым классом Person в пакете.

Напишите простую программу для вывода ClassLoader, соответствующего объекту человека.

import com.acai.Person;

public class TestClassLoader {
    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(person.getClass().getClassLoader());
    }
}

Тест 1. Внедрение пакета jar в проект

проект импорта пакетов jar

Соответствующий вывод:

sun.misc.Launcher$AppClassLoader@18b4aac2

Видно, что класс, расположенный в ClassPath, загружается App ClassLoader.

Тест 2. Скопируйте пакет jar в каталог %JRE_HOME%/lib/ext.

Скопируйте в %JRE_HOME%/lib/ext

Соответствующий вывод:

sun.misc.Launcher$ExtClassLoader@4cc77c2e

Можно сделать вывод, что Extension ClassLoader отвечает за загрузку классов в каталоге %JRE_HOME%/lib/ext.

При загрузке класса Person он сначала пытается загрузить его с помощью App ClassLoader.

Из-за родительского режима делегирования он, наконец, делегируется Extension ClassLoader, а класс Person существует в каталоге %JRE_HOME%/lib/ext, за который он отвечает, поэтому выполняется операция загрузки класса.

Тест 3: добавьте пакет jar в путь загрузки Bootstrap ClassLoader

Добавить к пути загрузки Bootstrap ClassLoader

Используйте параметр: -Xbootclasspath/a:d:\acai-cl.jar, чтобы добавить пакет jar к пути загрузки Bootstrap ClassLoader.

Соответствующий вывод:

null

Видно, что загрузка класса Person, наконец, делегирована Bootstrap ClassLoader.


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

Все еще оригинальная простая демонстрация.

import com.acai.Person;

public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(person.getClass().getClassLoader());
    }
}

Точка останова на методе loadClass класса ClassLoader.

App ClassLoader пытается загрузить Расширение ClassLoader пытается загрузить Bootstrap ClassLoader пытается загрузить

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

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

Затем попробуйте загрузить специальный класс: Splash.class.

Класс Splash находится в файле jfxrt.jar, упакованном в каталог %JRE_HOME%/lib/ext.

import com.sun.javafx.applet.Splash;

public class ExtTest {
    public static void main(String[] args) {
        Splash splash = new Splash(null);
        System.out.println(splash.getClass().getClassLoader());
    }
}

Соответствующий вывод:

sun.misc.Launcher$ExtClassLoader@330bedb4

Нет никаких сомнений в том, что класс Splash должен загружаться с помощью Extension ClassLoader.

Но процесс его загрузки все равно начнется с загрузчика системных классов по умолчанию App ClassLoader.

Вы можете просмотреть его через отладку.

App ClassLoader пытается загрузить

Процесс загрузки классов Splash будет делегирован Bootstrap ClassLoader, но Bootstrap ClassLoader не отвечает за загрузку классов в каталоге %JRE_HOME%/lib/ext. Наконец загружается с помощью Extension ClassLoader.

Bootstrap ClassLoader пытается загрузить безуспешно Окончательно загружен Extension ClassLoader

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

И думайте, что Bootstrap ClassLoader является родительским загрузчиком Extension ClassLoader.

Расширение ClassLoader является родительским загрузчиком App ClassLoader.

App ClassLoader — это родительский загрузчик пользовательского загрузчика классов.

Такое объяснение в основном правильное, но связь между Bootstrap ClassLoader и Extension ClassLoader требует дополнительного объяснения.

Механизм родительского делегирования, изображение из ссылки 7

Поскольку Bootstrap ClassLoader написан не на Java, родительский объект Extension ClassLoader не может быть указан как Bootstrap ClassLoader.

Этот уровень отношений создается в методе loadClass ClassLoader.

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

А родительский загрузчик null может быть только Extension ClassLoader.

Процесс может ссылаться на метод loadClass объекта ClassLoader.


Зачем использовать модель родительского делегирования?

В Интернете есть много примеров о классе String. Предположим, вы сами пишете класс java.lang.String. Использование родительского шаблона делегирования может предотвратить эту проблему.

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

Здесь попробуйте настроить класс java.lang.String и использовать пользовательский ClassLoader для загрузки.

package java.lang;

public class String {
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class StringClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        if ("java.lang.String".equals(name)) {
            return findClass(name);
        } else {
            return super.loadClass(name);
        }
    }

    @Override
    public Class<?> findClass(String s) throws ClassNotFoundException {
        try {
            byte[] classBytes = Files.readAllBytes(Paths.get("d:/String.class"));
            return defineClass(s, classBytes, 0, classBytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(s);
        }
    }

    public static void main(String[] args) throws ClassNotFoundException {
        StringClassLoader stringClassLoader = new StringClassLoader();
        Class clazz = stringClassLoader.loadClass("java.lang.String", false);
        System.out.println(clazz.getClassLoader());
    }
}

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

Результат:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang

Как видите, в defineClass, вызываемом методом findClass, есть такой абзац:

if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
        }

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

Если java.lang.String загружается Bootstrap ClassLoader в соответствии с онлайн-оператором, пользовательский загрузчик классов в демонстрации будет пропущен, и исключение не будет выведено.

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

Пятое: Пользовательский ClassLoader

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

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

  1. Загружать классы из сетевых файлов.
  2. Загружать классы из произвольных каталогов.
  3. Зашифруйте файл байт-кода и расшифруйте его с помощью пользовательского загрузчика классов.

Основные шаги для реализации пользовательского загрузчика классов:

  1. Наследуйте класс ClassLoader. Если вы загружаете классы только из каталога или пакета jar, вы также можете наследовать класс URLClassLoader.
  2. Переопределите метод findClass.
  3. В переписанном методе findClass независимо от того, какой метод используется, получается массив байтов, соответствующий файлу класса, а затем вызывается метод defineClass для преобразования его в экземпляр класса.

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

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

Действительно, в разрушении модели родительского делегирования есть некоторая практическая ценность.

Например, есть два файла классов или два пакета jar.

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

Существует два класса Person, оба из которых имеют полное имя класса com.acai.Person, с той лишь разницей, что выходные данные метода sayHello() немного отличаются.

package com.acai;

import lombok.Data;

@Data
public class Person {

    private String name;

    private Integer age;

    public void sayHello() {
        System.out.println("Hello, this is Person in acai-cl");
    }
}
package com.acai;

import lombok.Data;

@Data
public class Person {

    private String name;

    private Integer age;

    public void sayHello() {
        System.out.println("Hello, this is Person in acai-cl2");
    }
}

Введите проекты, в которых находятся два человека, в пакет jar.

две упаковки банок

Обычной операцией является импорт обоих пакетов jar в проект.

Напишите небольшое демо.

import com.acai.Person;

public class Main {

    public static void main(String[] args) throws Exception {
        Person person = new Person();
        System.out.println(person.getClass().getClassLoader());
        person.sayHello();
    }
}

Соответствующий вывод:

sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl

Как видите, класс Person в acai-cl.jar используется в демоверсии по умолчанию.

Если вы хотите использовать класс Person в acai-cl2.jar, вам нужно создать новый ClassLoader.

Если вам нужно загрузить классы из пакетов jar, URLClassLoader является приоритетом.

import com.acai.Person;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class Main {

    public static void main(String[] args) throws Exception {
        Person person = new Person();
        System.out.println(person.getClass().getClassLoader());
        person.sayHello();

        URL url = new File("d:/acai-cl2.jar").toURI().toURL();
        URLClassLoader loader = new URLClassLoader(new URL[]{url});
        Thread.currentThread().setContextClassLoader(loader);
        Class<?> clazz = loader.loadClass("com.acai.Person");
        System.out.println(clazz.getClassLoader());
        Method method = clazz.getDeclaredMethod("sayHello");
        method.invoke(clazz.newInstance());
    }
}

Соответствующий вывод:

sun.misc.Launcher$AppClassLoader@18b4aac2 Hello, this is Person in acai-cl

sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl

Можно видеть, что даже если указано использование acai-cl2.jar, вывод по-прежнему будет sayHello of Person в acai-cl.jar.

Причина в том, что два класса Person имеют одно и то же полное имя класса.

При загрузке второго Person обнаруживается, что загрузчик родительского класса App ClassLoader пользовательского загрузчика классов загрузил com.acai.Person.

Поэтому верните этот класс напрямую, который является классом Person в acai-cl.jar.

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

import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandlerFactory;

public class MyClassLoader extends URLClassLoader {


    public MyClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public MyClassLoader(URL[] urls) {
        super(urls);
    }

    public MyClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(urls, parent, factory);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (name.equals("com.acai.Person")) {
            return super.findClass(name);
        } else {
            return super.loadClass(name);
        }
    }
}

Метод loadClass переписан в MyClassLoader: когда имя загруженного класса равно com.acai.Person, метод findClass вызывается напрямую, минуя механизм родительского делегирования.

Здесь требуется суждение if, указывающее, что родительская делегация уничтожается только при загрузке com.acai.Person.

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

Родительским классом Person является java.lang.Object.

Загрузка класса Object напрямую с помощью пользовательского загрузчика классов вызовет исключение SecurityException.

Затем напишите демонстрацию.

import com.acai.Person;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class Main {

    public static void main(String[] args) throws Exception {
        Person person = new Person();
        System.out.println(person.getClass().getClassLoader());
        person.sayHello();

        URL url = new File("d:/acai-cl2.jar").toURI().toURL();
        URLClassLoader loader = new URLClassLoader(new URL[]{url});
        Thread.currentThread().setContextClassLoader(loader);
        Class<?> clazz = loader.loadClass("com.acai.Person");
        System.out.println(clazz.getClassLoader());
        Method method = clazz.getDeclaredMethod("sayHello");
        method.invoke(clazz.newInstance());

        URL url2 = new File("d:/acai-cl2.jar").toURI().toURL();
        MyClassLoader myClassLoader = new MyClassLoader(new URL[]{url2});
        Class<?> clazz2 = myClassLoader.loadClass("com.acai.Person");
        System.out.println(clazz2.getClassLoader());
        Method method2 = clazz2.getDeclaredMethod("sayHello");
        method2.invoke(clazz2.newInstance());
    }
}

sun.misc.Launcher$AppClassLoader@18b4aac2 Hello, this is Person in acai-cl

sun.misc.Launcher$AppClassLoader@18b4aac2 Hello, this is Person in acai-cl

MyClassLoader@5e2de80c
Hello, this is Person in acai-cl2

Видно, что класс Person в Acai-cl2.jar загружен правильно.

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

Шесть: ссылки

[1] Java Garbage Collection Basics
[2] Как загрузчик классов java загружает себя в память для выполнения?
[3] Подробный и глубокий анализ механизма работы Java ClassLoader
[4] Углубленный анализ принципа Java ClassLoader
[5] Глубокое погружение в загрузчики классов Java
[6] Углубленный анализ механизма Java ClassLoader (уровень исходного кода)
[7] Принцип загрузки классов Java и краткое изложение использования ClassLoader
[8] Реализовать загрузчик классов java для динамической загрузки пакетов jar.
[9] Подробное объяснение основ ClassLoader