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

Java

В предыдущей статье была представлена ​​статическая структура байт-кода класса. Эти классы должны быть загружены в область данных времени выполнения, выделенную jvm в памяти, чтобы они вступили в силу. Этот процесс включает в себя:加载 -> 链接 -> 初始化несколько этапов, из которых этап ссылки имеет验证 -> 准备 -> 解析Три части. Далее я буду использовать три статьи, чтобы подробно представить эти три этапа. Эта статья сначала знакомит с загрузкой классов jvm и концепцией модели родительского делегирования.

Примечание: загрузка класса включает в себя поток от байт-кода до области метода jvm.java.lang.ClassВесь процесс создания и инициализации объекта, а также загрузки классов, описанный в этой статье, является лишь одним из этапов.

область данных времени выполнения

Содержание этой статьи основано на горячих точках jvm, а объект класса хранится в области методов во время выполнения.

время загрузки класса

Когда вы начинаете загружать байт-код класса? Эта спецификация jvm не дает четкого определения, но спецификация jvm четко определяет время инициализации класса.В соответствии с тем, что загрузка класса происходит до его инициализации, условия срабатывания для загрузки класса могут быть изменены.

Примечание. Класс в этой статье относится к общему термину, включая интерфейсы и т. д.

Спецификация jvm определяет, что есть только 5 случаев, в которых инициализация класса будет запущена, если класс не был инициализирован:

  • Основной класс (класс, содержащий основной метод), указанный при запуске виртуальной машины, загружается и инициализируется первым.
  • При инициализации класса слов рекурсивно инициализируйте его родительский класс
  • При выполнении инструкций new, getstatic, putstatic, invokestatic
  • При использовании класса через вызовы отражения
  • java.lang.invoke.MethodHandleРезультат анализа экземпляраREF_getStatic, REF_putStatic, REF_invokeStaticдескриптор метода

Ситуации выше понятны.Текущий класс ссылается на некий класс и использует его.Естественно его нужно инициализировать и загрузить.Можно передать-XX:+TraceClassLoadingПросмотр загруженных классов.Про инициализацию будущие статьи будут подробно представлены отдельно

загрузчик классов

Загрузка класса заключается в загрузке статической структуры байт-кода класса через jvm и создании соответствующегоjava.lang.ClassОбъект хранится в собственном пространстве памяти области метода времени выполнения, после чего доступ к данным этого класса осуществляется через объект класса, включая его поля класса, методы и т. д.

Байт-код не ограничивается только файлами в локальной файловой системе, но может быть и в памяти (динамически генерируемый), в сети, в сжатых пакетах (jar, war) и т. д., а загрузка байтов входит в обязанности загрузчика классов из этих мест код в файл jvm.

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

  • загрузчик классов начальной загрузки: загрузить$JAVA_HOME/jre/lib/Основная библиотека классов, такая как rt.jar, реализована на C++ в Hotspot jvm.

  • Загрузчики пользовательских классов: все загрузчики пользовательских классов наследуютjava.lang.ClassLoaderАбстрактный класс, солнце предоставляет два загрузчика пользовательских классов, мы также можем определить свои собственные загрузчики классов.

    • Расширенный загрузчик классов (ExtClassLoader):sun.misc.Launcher$ExtClassLoader, отвечает за загрузку$JAVA_HOME/jre/lib/extНекоторые классы расширения в

    • Загрузчик класса приложения (AppClassLoader): может бытьClassLoader.getSystemClassLoader()Метод получения, также известный как загрузчик системных классов, отвечает за загрузку определенных пользователем (в пути к классам) классов.

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

Некоторые дополнительные моменты о загрузчиках классов:

  1. Реализованы загрузчики пользовательских классовjava.lang.ClassLoaderабстрактный класс, другой классprivate final ClassLoader parentПоля представляют родительский загрузчик загрузчика (такая композиция рекомендуется вместо наследования в шаблонах проектирования), что является ключом к реализации модели родительского делегирования.
  2. Загрузчик классов загрузки загружает классы в каталог jre lib (или указанный -Xbootclasspath) и распознает только определенные имена файлов, такие какrt.jar, поэтому класс пользователя не будет загружен
  3. Для массивов нет представления байт-кода типа массива, он создается jvm, как правило, при встречеnewarrayКогда инструкция инициализируется, если тип элемента массива не является примитивным типом (например, int[]), а ссылочным типом (например, Integer[]), примитивный тип будет загружен первым, что может быть определено загрузчиком классов начальной загрузки или пользовательским загрузчиком классов Load, в зависимости от типа ссылки.
  4. JVM кэширует загруженные классы и устанавливает загрузчик для загрузки соответствующего класса, см. ниже
  5. Класс и загружающий его загрузчик классов (определяющий загрузчик классов) совместно определяют уникальность класса.

Давайте посмотрим на пример ниже:

/**
 * 自定义类加载器,重写loadClass,优先在当前目录加载
 */
public class MyClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            InputStream is = new FileInputStream("./" + name + ".class");
            byte[] data = new byte[is.available()];
            is.read(data);
            return defineClass(name, data, 0, data.length);
        } catch (IOException e) {
            return super.loadClass(name);
        }
    }
}
public class Callee {
    public Callee() {
        System.out.println("Callee class loaded by " + this.getClass().getClassLoader().getClass().getName());
    }
}
/**
 * Run: javac MyClassLoader.java Callee.java Test.java && java Test
 * /
public class Test {
    public static void main(String[] args) throws Exception {
        ClassLoader myClassLoader = new MyClassLoader();

        Class<?> calleeClass = myClassLoader.loadClass("Callee");

        //输出:calleeClass == Callee.class ? false
        System.out.println("calleeClass == Callee.class ? " + (calleeClass == Callee.class));

        //输出:Callee class loaded by sun.misc.Launcher$AppClassLoader
        Callee.class.newInstance();
        //输出:Callee class loaded by MyClassLoader
        Object calleeObj = calleeClass.newInstance();

    }
}

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

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

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

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

Можно видеть, что загрузчик класса, который получает запрос на загрузку первым, не обязательно загружает класс, он может быть загружен его родительским загрузчиком, а загрузчик класса, который получает запрос на загрузку, вызывается初始类加载器(initiating loader), а загрузчик классов, завершающий загрузку, называется定义类加载器(defining loader), начальный загрузчик классов и определяющий загрузчик классов могут совпадать, а могут и не совпадать.

Если два класса: D ссылается на C, L1 используется в качестве загрузчика класса определения D, C будет загружен при анализе D, этот запрос загрузки будет получен L1, предполагая, что C загружен другим загрузчиком L2, L1 в конечном итоге загрузит запрос делегируется L2, L1 называется начальным загрузчиком C, а L2 является загрузчиком класса определения C.

Давайте посмотрим, как ClassLoader реализует загрузку родительского делегирования:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
    	 // 一个类的加载是放在代码同步块里边的,所以不会有同一个类加载多次
        synchronized (getClassLoadingLock(name)) {
            // 首先检查该类是否已加载过
            Class<?> c = findLoadedClass(name);
            // 如果缓存中没有找到,则按双亲委派模型加载
            if (c == null) {
                try {
                    if (parent != null) {
                    	// 如果父加载器不为null,则代理给父加载器加载
                    	// 父加载器在自己搜索范围内找不到该类,则抛出ClassNotFoundException
                        c = parent.loadClass(name, false);
                    } else {
                    	// 如果父加载器为null,则从引导类加载器加载过的类中
                    	// 找是否加载过此类,找不到返回null
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 存在父加载器但父加载器没有找到要加载的类触发此异常
                    // 只捕获不处理,交给字加载器自身去加载
                }

                if (c == null) {
                    // 如果从父加载器到顶层加载器(引导类加载器)都找不到此类,则自己来加载
                    c = findClass(name);
                }
            }
            
            // 如果resolve指定为true,则立即进入链接阶段
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

Из исходного кода видно, что все классы сначала делегируются родительскому загрузчику для загрузки.Если родительский загрузчик не может быть загружен, он будет загружен сам.Логика очень проста.Преимущество этого в том, что есть нет необходимости передавать иерархические классы разным загрузчикам.java.lang.IntegerВ конечном итоге он загружается Bootstrap ClassLoader, поэтому будет загружен только один из тех же классов.

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

  • getClassLoadingLock
protected Object getClassLoadingLock(String className) {
    Object lock = this;
    if (parallelLockMap != null) {
        Object newLock = new Object();
        lock = parallelLockMap.putIfAbsent(className, newLock);
        if (lock == null) {
            lock = newLock;
        }
    }
    return lock;
}

Метод прост, parallelLockMap — этоConcurrentHashMap<String, Object>map, если текущий загрузчик классов зарегистрирован как загружаемый параллельно, объект блокировки поддерживается для каждого имени класса дляsynchronizedЕсли он используется, разные классы могут загружаться параллельно, в противном случае текущий загрузчик классов используется в качестве объекта блокировки и может загружаться только последовательно.

  • findBootstrapClassOrNull
private Class<?> findBootstrapClassOrNull(String name)
{
   if (!checkName(name)) 
   		return null;
   
   return findBootstrapClass(name);
}
private native Class<?> findBootstrapClass(String name);

findBootstrapClass — собственная реализация jvm, находит класс, загруженный Bootstrap ClassLoader, если нет, возвращает null

  • findClss
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

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

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

/**
 * Run: javac MyClassLoader.java Callee.java Test.java && java Test
 */
public class MyClassLoader extends ClassLoader {

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            InputStream is = new FileInputStream("./" + name + ".class");
            byte[] data = new byte[is.available()];
            is.read(data);
            return defineClass(name, data, 0, data.length);
        } catch (IOException e) {
            return super.loadClass(name);
        }
    }
}

public static void main(String[] args) throws Exception {
    ClassLoader myClassLoader = new MyClassLoader();
    Class<?> callerClass = myClassLoader.loadClass("Callee");
    // 输出:Callee class loaded by sun.misc.Launcher$AppClassLoader
    callerClass.newInstance();
}

Как видите, просто измените имя предыдущего метода экземпляра наfindClassВот и все, и вы можете видеть, что загрузчик класса приложения отвечает за загрузку (родительский загрузчик по умолчанию — AppClassLoader), что соответствует модели родительского делегирования.

Проведите еще один эксперимент:

// 让自定义类加载器加载/tmp目录下的类
InputStream is = new FileInputStream("/tmp/" + name + ".class");

только что скомпилированоCallee.classперейти к/tmpДалее (примечание: не сохраняйте копию текущего каталога):

 mv Callee.class /tmp

Скомпилируйте и запустите снова:

javac MyClassLoader.java && java Test

результат:

Callee class loaded by MyClassLoader

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

Проведем еще один интересный эксперимент:

определить классCallerназывается внутриCallee:

public class Caller {
    public Caller() {
        System.out.println("Caller class loaded by " + this.getClass().getClassLoader().getClass().getName());
        Callee callee = new Callee();
    }
}

Измените Test.java, загрузитеCaller

Class<?> callerClass = myClassLoader.loadClass("Caller");

Скомпилируйте и запустите снова:

javac MyClassLoader.java Caller.java Test.java
mv Callee.class /tmp # 保证当前目录下没有Callee.class,/tmp下有
java Test

Почему в /tmpCallee.classа не грузится в него? На самом деле это легко понять: выведите первое предложение, чтобы увидетьAppClassLoaderзагруженCaller.class, как его определенный загрузчик классов, когда Caller используется в Caller, его необходимо загрузитьCallee.classкогда,AppClassLoaderбудет действовать какCallee.classначальный загрузчик для его загрузки в соответствии с родительской моделью делегирования и, наконец,AppClassLoaderпозвони своемуfindClassПопробуйте загрузить его сами, такого класса нет в пути к классам и его нельзя найти~

Этот пример также можно увидеть: загрузчик класса, который фактически загружает класс (вызывая метод findClass), не может найти класс и выдаетClassNotFoundException, это исключение инкапсулируется какNoClassDefFoundErrorБрошенный в место использования (начальный загрузчик классов), такая ошибка встречается часто.

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

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

Например, два загрузчика L1, L2 (L1 расширяет L2), L2 загружает класс A, а класс A использует класс B (класс B находится в пределах диапазона поиска L1 и должен быть загружен L1), тогда L2 используется как начальный. загрузка загрузчика класса B и делегирование родительскому загрузчику для загрузки, в конечном итоге родительский загрузчик не загружается, и L2 пытается загрузить себя. Вполне возможно, что L2 не может найти класс B в своем собственном диапазоне поиска и, в конце концов, не может загрузиться.

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

  • Thread Context Class Loader

загрузчик контекста потока, Наиболее типичным сценарием применения является технология SPI, такая как JDBC, JNDI, JAXP и т. д. Спецификация интерфейса определяется основной библиотекой классов java, а конкретная реализация спецификации предоставляется разными производителями, которые должны быть в код библиотеки классов.При вызове пользовательского кода это необходимо делать через загрузчик контекста потока.

через объект потокаsetContextClassLoaderМетод устанавливает текущий загрузчик контекста потока.Если он не установлен, он наследуется от родительского потока.Если родительский поток не установил его, то в качестве загрузчика контекста потока используется загрузчик класса приложения (AppClassLoader).

//ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}
  • механизм загрузки класса tomcat

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

  1. Общие библиотеки классов (например, servlet-api.jar) должны быть общими.
  2. Разные приложения могут полагаться на разные версии одного и того же класса, а разные приложения изолированы друг от друга и не влияют друг на друга.

Поняв цель, давайте посмотрим, какие меры предпринял tomcat:

В этой статье не будет представлен исходный код, в будущем я напишу серию исходных кодов tomcat.

Примечание. Различные реализации jvm не совпадают, Bootstrap здесь относится к Bootstrap и ExtClassloader в точке доступа.

Это архитектура до tomcat6, common, Server (Catalina), Shared загружает классы в tomcat /common, /server, /shared соответственно, но в текущей версии (tomcat9), если server.loader настроен, shared.loader еще применимо.

Если конфигурации нет, tomcat по-прежнему создает три загрузчика классов: commonLoader, catalinaLoader и sharedLoader (все они являются экземплярами общего загрузчика классов, загружающими классы в каталог /lib), поэтому общая архитектура выглядит следующим образом:

Теперь посмотрите еще раз:

  1. Общий загрузчик следует родительской модели загрузки, а базовая библиотека классов не повторяется.
  2. Загрузчик WebappX соответствует одному для каждого приложения, загружая/WEB-INF/classes, /WEB-INF/lib/*Классы под, Изоляция уровня приложения

Проблема решена отлично, порядок загрузки WebappX:

  1. Сначала передайте его загрузчику Bootstrap для загрузки
  2. Найдите и загрузите в application/WEB-INF/classes
  3. Найти загрузки в приложениях /WEB-INF/lib/*.jar
  4. Передача на загрузку класса System для загрузки (ClassPath)
  5. Передать загрузчику классов Common для загрузки (/lib)

Видно, что приоритет загрузки классов в каталогах /class и /lib выше, чем у системного загрузчика классов и общего загрузчика классов.

можно настроить<Loader delegate="true"/>Принудительно загрузить его в соответствии с моделью родительского делегирования

Оригинальный адрес:оригинальный

Прошлый контент:

Подробное объяснение файла байт-кода (класса)

прочитать файл класса

Добро пожаловать, чтобы обратить внимание!