Принцип загрузки классов Java и краткое изложение использования ClassLoader

Java

предисловие

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

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

Концепцию загрузки класса следует рассматривать как инновацию языка Java. Цель состоит в том, чтобы отделить процесс загрузки класса от виртуальной машины и достичь цели «получения двоичного потока байтов, описывающего этот класс, через полное имя сорт". Модуль кода, реализующий эту функциональность, — загрузчик классов. Базовой моделью загрузчика классов является знаменитая модель родительского делегирования. Звучит здорово, но логика очень проста. Когда нам нужно загрузить класс, мы сначала оцениваем, был ли загружен класс. Если нет, оцениваем, был ли он загружен родительским загрузчиком. Если нет, вызываем наш собственный findClass метод. попробуйте загрузить. Базовая модель такова (пиратство, вторжение и удаление):

Это также очень просто реализовать.Основное внимание уделяется методу loadClass класса ClassLoader.Исходный код выглядит следующим образом:

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) {
            
            Class(c);
        }
        return c;
    }
}

Взгляните на метод findClass, когда вам нечего делать:

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

Я вдруг почувствовал удивление, почему по умолчанию выбрасывается исключение? На самом деле, поскольку класс ClassLoader является абстрактным классом, при его использовании на самом деле создается подкласс Этот метод будет переписан по мере необходимости для завершения процесса загрузки, требуемого бизнесом.

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

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

Уничтожить родительскую модель делегирования

Зачем нарушать модель родительского делегирования?
На самом деле, в некоторых случаях нам может понадобиться загрузить два разных класса, но, к сожалению, имена двух классов абсолютно одинаковы.В настоящее время модель родительского делегирования не может удовлетворить наши требования, и мы должны переписать метод loadClass. нарушает модель родительского делегирования, позволяя загружать одно и то же имя класса несколько раз. Конечно, упомянутый здесь ущерб — это только ущерб в локальном смысле.
Но имена классов одинаковые, как JVM различает эти два класса? Очевидно, что это не приведет к краху представления мира, ведь класс в JVM не только ограничен именем класса, но и принадлежит ClassLoader, который его загружает. Классы, загруженные разными загрузчиками классов, фактически независимы друг от друга.
Сделай эксперимент.
Начнем с написания двух классов:

package com.mythsman.test;
public class Hello {
    public void say() {
        System.out.println("This is from Hello v1");
    }
}

package com.mythsman.test;
public class Hello {
    public void say() {
        System.out.println("This is from Hello v2");
    }
}

Два класса имеют одинаковое имя, единственная разница заключается в реализации методов. Мы сначала компилируем отдельно, а затем переименовываем сгенерированные файлы классов в Hello.class.1 и Hello.class.2.
Мы намерены создать отдельные экземпляры этих двух классов в тестовом классе.
Затем мы создаем новый тестовый класс com.mythsman.test.Main и два пользовательских ClassLoader в функции main:

ClassLoader classLoader1=new ClassLoader() {
    @Override
    public Class<?> loadClass(String s) throws ClassNotFoundException {
        try {
            if (s.equals("com.mythsman.test.Hello")) {
                byte[] classBytes = Files.readAllBytes(Paths.get("/home/myths/Desktop/test/Hello.class.1"));
                return defineClass(s, classBytes, 0, classBytes.length);
            }else{
                return super.loadClass(s);
            }
        }catch (IOException e) {
            throw new ClassNotFoundException(s);
        }
    }
};
ClassLoader classLoader2=new ClassLoader() {
    @Override
    public Class<?> loadClass(String s) throws ClassNotFoundException {
        try {
            if (s.equals("com.mythsman.test.Hello")) {
                byte[] classBytes = Files.readAllBytes(Paths.get("/home/myths/Desktop/test/Hello.class.2"));
                return defineClass(s, classBytes, 0, classBytes.length);
            }else{
                return super.loadClass(s);
            }
        }catch (IOException e) {
            throw new ClassNotFoundException(s);
        }
    }
};

Цель этих двух загрузчиков классов — связать два разных байт-кода класса Hello соответственно.Нам нужно прочитать файл байт-кода и загрузить его в класс с помощью метода defineClass. Обратите внимание, что мы перегружаем метод loadClass.Если метод findClass перегружен, метод findClass второго ClassLoader фактически не будет вызываться из-за механизма обработки родительского делегирования метода loadClass.
Итак, как мы генерируем экземпляры? Очевидно, что мы не можем ссылаться на имя класса напрямую (конфликт имен), поэтому мы можем использовать только отражение:

Object helloV1=classLoader1.loadClass("com.mythsman.test.Hello").newInstance();
Object helloV2=classLoader2.loadClass("com.mythsman.test.Hello").newInstance();
helloV1.getClass().getMethod("say").invoke(helloV1);
helloV2.getClass().getMethod("say").invoke(helloV2);

вывод:

This is from Hello v1
This is from Hello v2

Хорошо, даже если две загрузки завершены, есть еще несколько моментов, на которые следует обратить внимание.

Какая связь между двумя классами

Очевидно, что эти два класса не являются одним и тем же классом, но у них одно и то же имя, так что же получается в результате таких операций, как isinstance of:

System.out.println("class:"+helloV1.getClass());
System.out.println("class:"+helloV2.getClass());
System.out.println("hashCode:"+helloV1.getClass().hashCode());
System.out.println("hashCode:"+helloV2.getClass().hashCode());
System.out.println("classLoader:"+helloV1.getClass().getClassLoader());
System.out.println("classLoader:"+helloV2.getClass().getClassLoader());

вывод:

class:class com.mythsman.test.Hello
class:class com.mythsman.test.Hello
hashCode:1581781576
hashCode:1725154839
classLoader:com.mythsman.test.Main$1@5e2de80c
classLoader:com.mythsman.test.Main$2@266474c2

Их имена классов действительно одинаковы, но хэш-коды классов разные, а это означает, что они не являются одним и тем же классом по сути, и их загрузчики классов также различны (фактически, два внутренних класса Main).

Какова связь между этими двумя загрузчиками классов и трехуровневыми загрузчиками классов системы?

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

System.out.println(classLoader1.getParent().getParent().getParent());
System.out.println(classLoader1.getParent().getParent());
System.out.println(classLoader1.getParent());
System.out.println(classLoader1 );
System.out.println(ClassLoader.getSystemClassLoader());

вывод:

null
sun.misc.Launcher$ExtClassLoader@60e53b93
sun.misc.Launcher$AppClassLoader@18b4aac2
com.mythsman.test.Main$1@5e2de80c
sun.misc.Launcher$AppClassLoader@18b4aac2

Мы видим, что четвертая строка — это пользовательский ClassLoader, его отец — AppClassLoader, дедушка — ExtClassLoader, а дедушка — null, на самом деле это BootStrapClassLoader, написанный на C. ClassLoader текущей системы — это AppClassLoader.
Конечно, отношения родитель-потомок, упомянутые здесь, являются не отношениями наследования, а комбинированными отношениями.Дочерний ClassLoader сохраняет ссылку на родительский ClassLoader (родительский).

Есть ли более элегантный способ вызова без отражения?

Очевидно, что использовать рефлексию, чтобы вызывать его каждый раз, все же слишком глупо, нет ли более удобного метода, такого как обращение по имени класса? Конечно есть.Причина, по которой имя класса нельзя использовать непосредственно перед, заключается в том, что загрузчиком класса нативного класса является systemClassLoader, а загрузчиком класса, созданного из файла класса, является пользовательский classLoader.Суть этих двух классы разные.Поэтому они не могут быть приведены друг к другу.Если их принудительно привести, будет сообщено об исключении ClassCastException. Итак, если мы извлекаем родительский класс, родительский класс загружается с помощью systemClassLoader, а подкласс загружается с помощью пользовательского classLoader, а затем преобразуется в родительский класс при принудительном преобразовании, разве это не хорошо?
Чтобы провести эксперимент, создайте родительский класс Отца, фактически извлеките абстрактный метод:

package com.mythsman.test;
public abstract class Father {
    public abstract void say();
}

Затем измените класс Hello:

package com.mythsman.test;
public class Hello extends Father {
    @Override
    public void say() {
        System.out.println("say outside");
    }
}

Затем вручную скомпилируйте класс Hello и поместите файл класса в другое место. Переработайте класс, заменив «скажи снаружи» на «скажи внутри».
Затем измените основную функцию:

package com.mythsman.test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class Main {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader classLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String s) throws ClassNotFoundException {
                try {
                    if (s.equals("com.mythsman.test.Hello")) {
                        byte[] classBytes = Files.readAllBytes(Paths.get("/home/myths/Desktop/test/Hello.class"));
                        return defineClass(s, classBytes, 0, classBytes.length);
                    } else {
                        return super.loadClass(s);
                    }
                } catch (IOException e) {
                    throw new ClassNotFoundException(s);
                }
            }
        };
        Father outside = (Father) classLoader.loadClass("com.mythsman.test.Hello").newInstance();
        Hello inside = new Hello();
        outside.say();
        inside.say();
    }
}

Итак, мы видим, что вывод:

say outside
say inside


Название этой статьи: Принцип загрузки классов Java и краткое изложение использования ClassLoader

Количество слов в статье: 2236. Время чтения: 5 минут. Редактировать ссылку:GITHUB

Опубликовано: 17 декабря 2017 г.

соглашение:Attribution-NonCommercial-ShareAlike 4.0