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

JVM

[toc]

механизм загрузки классов

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

жизненный цикл класса

Класс начинается с загрузки в память виртуальной машины и заканчивается выгрузкой из памяти.Весь его жизненный цикл включает в себя: загрузку, проверку, подготовку, синтаксический анализ, инициализацию, использование, выгрузку.Эти этапы 7. Среди них три части проверки, подготовки и разбора в совокупности называются соединением.

image-20200827153624097

Определен порядок пяти фаз загрузки, проверки, подготовки, инициализации и выгрузки, и процесс загрузки типа должен начинаться шаг за шагом в этом порядке, в то время как фаза синтаксического анализа не обязательно: в некоторых случаях она может быть после фаза инициализации Опять же, это для поддержки функции связывания во время выполнения языка Java (также известной как динамическое связывание или позднее связывание).

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

load: найти и загрузить бинарные данные класса

На этапе загрузки виртуальная машина должна сделать три вещи:

  • 1) Получить двоичный поток байтов, определяющий этот класс по его полному имени.
  • 2) Преобразуйте статическую структуру хранения, представленную этим потоком байтов, в структуру данных времени выполнения области метода.
  • 3) Создайте объект java.lang.Class, представляющий этот класс в памяти в качестве записи доступа к различным данным этого класса в области методов.

image-20200827112719502

Проверка: убедитесь в правильности загруженных классов.

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

  • Проверка формата файла: убедитесь, что поток байтов соответствует спецификации формата файла класса, например: начинается ли он с0xCAFEBABEStart, находятся ли основной и дополнительный номера версий в пределах диапазона обработки текущей виртуальной машины и имеют ли константы в пуле констант неподдерживаемые типы.
  • Проверка метаданных: семантический анализ информации, описанной байт-кодом (примечание: отличиеjavacсемантический анализ на этапе компиляции), чтобы убедиться, что описываемая им информация соответствует требованиям спецификации языка Java; например: имеет ли этот класс родительский класс, за исключениемjava.lang.Objectза пределами.
  • Проверка байт-кода: посредством анализа потока данных и потока управления определено, что семантика программы является законной и логичной.
  • Проверка символических ссылок: чтобы убедиться, что операция анализа выполняется правильно.

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

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

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

Примечания на этом этапе:

  • На этот раз включает в себя только переменные классов распределения памяти (статические переменные, которые должны быть изменены), и не включает в себя переменные экземпляра, переменные экземпляра будут назначены вместе с объектом, когда объект создается в куче в Java.
  • Установленное здесь начальное значение обычно является нулевым значением по умолчанию для типа данных (например,0,0L,null,falseи т. д.), а не явно присваиваемые значения в коде Java.

Например: предположим, что переменная класса определена как:public static int value = 3Тогда значение переменной является начальным значением после этапа подготовки.0, вместо3, так как ни один метод Java еще не был выполнен, и значение присваивается 3put staticИнструкции хранятся в конструкторе класса после компиляции программы.()метод, поэтому действие присвоения значения 3 не будет выполнено до фазы инициализации.

  • Для базовых типов данных, для переменных класса (статических) и глобальных переменных, если они используются напрямую без их явного присвоения, система присвоит им значение по умолчанию, равное нулю, в то время как для локальных переменных перед использованием необходимо явно присвоить, иначе он не пройдет во время компиляции.

  • для одновременногоstaticиfinalМодифицируемые константы должны быть явно назначены во время объявления, иначе они не будут переданы во время компиляции, в то время как константы, которые изменены только final, могут быть явно назначены во время объявления или во время инициализации класса. значение явно, короче говоря, оно должно быть явно назначено перед использованием, и система не присвоит ему нулевое значение по умолчанию.

  • Для справочных типов данныхreferenceНапример, такие как ссылки на массивы, ссылки на объекты и т. д., если они используются напрямую без явного назначения, система присвоит им нулевое значение по умолчанию, то естьnull.

  • Если при инициализации массива каждому элементу массива не присвоено значение, элементам в нем будет присвоено нулевое значение по умолчанию в соответствии с соответствующим типом данных.

  • Если атрибут ConstantValue существует в таблице атрибутов поля поля класса, то есть модифицируется как final, так и static, то значение переменной будет инициализировано значением, заданным атрибутом ConstValue на этапе подготовки. Предположим, что указанное выше значение переменной класса определено как:public static final int value = 3;При компиляции Javac будет генерировать свойство ConstantValue для значения. На этапе подготовки виртуальная машина присваивает значение 3 в соответствии с настройкой ConstantValue. Мы можем понять это какstatic finalКонстанта помещает свой результат в пул констант вызывающего класса во время компиляции.

Решение: преобразовать символические ссылки в классах в прямые ссылки.

Фаза синтаксического анализа — это процесс, посредством которого виртуальная машина заменяет символические ссылки в пуле констант прямыми ссылками., действие синтаксического анализа в основном дляили接口,字段,类方法,接口方法,方法类型,方法句柄и调用点Сделаны ссылки на символ класса классификатора 7. Символическая ссылка — это набор символов для описания цели, который может быть любым литералом.

直接引用Является указателем непосредственно на цель, относительным смещением или маркером, который косвенно расположен на цели.

Инициализация: выполнение операций инициализации над статическими переменными класса, статическими блоками кода

Инициализация, присвоение правильных начальных значений статическим переменным класса, JVM отвечает за инициализацию класса, в основном за инициализацию переменных класса. Есть два способа инициализировать переменные класса в Java:

  • Объявление переменной класса указывает начальное значение
  • Используйте статические блоки кода для присвоения начальных значений переменным класса

Этапы инициализации класса

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

Время инициализации класса триггера

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

  • При создании экземпляра объекта с использованием нового ключевого слова.

  • При чтении или установке статического поля типа (за исключением статических полей, измененных final, чьи результаты были помещены в пул констант во время компиляции).

  • При вызове статического метода типа.

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

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

  • Когда виртуальная машина запускается, пользователю необходимо указать основной класс для выполнения (тот, который содержит метод main()), и виртуальная машина сначала инициализирует этот основной класс.

Инициализация класса не будет выполняться в следующих случаях

  1. Ссылка на статические поля родительского класса через подкласс вызовет инициализацию только родительского класса, а не подкласса.

  2. Определение массива объектов не вызывает инициализацию класса.

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

Класс, в котором определена константа.

  1. Получение объекта Class по имени класса не приведет к инициализации класса.

  2. При загрузке указанного класса через Class.forName, если указанный параметр initialize имеет значение false, инициализация класса не будет запущена.

Инициализация, по сути, этот параметр сообщает виртуальной машине, следует ли инициализировать класс.

  1. Через метод loadClass по умолчанию для ClassLoader действие инициализации не будет запущено.

использовать

Интерфейс для класса для доступа к структуре данных в области методов, а объект — это данные в области кучи.

удалить

Несколько ситуаций, в которых виртуальная машина Java завершит свой жизненный цикл

  • Выполняется метод System.exit()
  • Выполнение программы завершается нормально
  • Программа сталкивается с исключением или ошибкой во время выполнения и аварийно завершается
  • Процесс виртуальной машины Java прерван из-за ошибки операционной системы

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

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

Команда дизайна виртуальной машины помещает «дифференцированный поток Birace, описанный в этапе нагрузки класса», чтобы описать этот тип двоичного байтового потока «к внешней стороне виртуальной машины Java, чтобы приложение определило, как получить требуемый класс. Модуль кода, который реализует это действие, называется «классовым погрузчиком».

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

image-20200827154815302

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

С точки зрения виртуальной машины Java существует только два разных загрузчика классов: один — Bootstrap ClassLoader, который реализован на C++ и является частью самой виртуальной машины, а другой — Bootstrap ClassLoader. , которые реализованы языком Java, независимо от внешней среды виртуальной машины, и все они наследуются от абстрактного класса java.lang.ClassLoader.

С точки зрения Java-разработчиков загрузчики классов можно разделить на более подробные сведения.Большинство программ Java используют загрузчики классов, предоставляемые следующими тремя системами:

Bootstrap ClassLoader

Этот класс будет храниться в каталоге \lib или по пути, указанному параметром -Xbootclasspath, и распознаваться виртуальной машиной (определяемой по имени файла, например, rt.jar, tools.jar, библиотека классов чье имя не совпадает, не будет загружен, даже если он будет помещен в каталог lib) Библиотека классов загружается в память виртуальной машины.

Расширение ClassLoader

Этот загрузчик реализован sun.misc.Launcher$ExtClassLoader, который отвечает за загрузку всех библиотек классов в каталог \lib\ext или по пути, указанному системной переменной java.ext.dirs. Разработчики могут напрямую использовать загрузчик класса расширения.

Загрузчик классов приложений

Этот загрузчик классов реализован sun.misc.Launcher$AppClassLoader. Поскольку загрузчик класса приложения является возвращаемым значением метода getSystem-ClassLoader() в классе ClassLoader, в некоторых случаях его также называют «загрузчиком системного класса».

Он отвечает за загрузку всех библиотек классов по пользовательскому пути к классам (ClassPath), и разработчики также могут использовать этот загрузчик классов непосредственно в коде. Если приложение не настроило свой собственный загрузчик классов, в общем случае это загрузчик классов по умолчанию в программе.

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

Три способа загрузки класса

  1. Когда командная строка запускает приложение, оно инициализируется и загружается JVM.
  2. Динамическая загрузка через метод Class.forName()
  3. Динамическая загрузка через метод ClassLoader.loadClass()

Пример кода:

public class loaderTest { 
        public static void main(String[] args) throws ClassNotFoundException { 
                ClassLoader loader = HelloWorld.class.getClassLoader(); 
                System.out.println(loader); 
                //使用ClassLoader.loadClass()来加载类,不会执行初始化块 
                loader.loadClass("Test2"); 
                //使用Class.forName()来加载类,默认会执行初始化块 
//                Class.forName("Test2"); 
                //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块 
//                Class.forName("Test2", false, loader); 
        } 
}

public class Test2 { 
        static { 
                System.out.println("静态初始化块执行了!"); 
        } 
}

Разница между Class.forName() и ClassLoader.loadClass()

  • Class.forName(): помимо загрузки файла .class класса в jvm, он также интерпретирует класс и выполняет статический блок в классе;

  • ClassLoader.loadClass(): сделайте только одно, то есть загрузите файл .class в jvm и не будете выполнять содержимое в статике, только в newInstance статический блок будет выполняться.

  • Функция Class.forName(name, initialize, loader) с параметрами также может управлять загрузкой статического блока. И только метод newInstance() вызывается вызовом конструктора для создания объекта класса.

Механизм загрузки классов JVM

Возьмите на себя полную ответственность

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

делегат родительского класса

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

механизм кэширования

Механизм кэширования гарантирует, что все загруженные классы будут кэшированы. Когда класс необходимо использовать в программе, загрузчик классов сначала ищет класс в области кеша. Только если область кеша не существует, система прочитать соответствующий класс класса двоичные данные, преобразовать их в объект класса и сохранить в буфере. Вот почему после изменения класса необходимо перезапустить JVM, чтобы изменение программы вступило в силу.

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

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

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

  1. Когда AppClassLoader загружает класс, он не пытается сначала загрузить сам класс, а делегирует запрос на загрузку класса загрузчику родительского класса ExtClassLoader для завершения.
  2. Когда ExtClassLoader загружает класс, он не пытается сначала загрузить сам класс, а делегирует запрос на загрузку класса BootStrapClassLoader для завершения.
  3. Если BootStrapClassLoader не загружается (например, класс не найден в $JAVA_HOME/jre/lib), для попытки загрузки будет использоваться ExtClassLoader;
  4. Если ExtClassLoader также не загружается, он будет использовать AppClassLoader для загрузки, если AppClassLoader также не загружается, будет сообщено об исключении ClassNotFoundException.

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

image-20200827135115768

Иерархическая связь между загрузчиками классов, показанная на рисунке выше, называется моделью родительского делегирования загрузчиков классов.Модель родительского делегирования требует, чтобы все загрузчики классов, кроме загрузчика классов запуска верхнего уровня, имели свои собственные загрузчики родительских классов. Здесь отношения родитель-потомок между загрузчиками классов, как правило, не реализуются в отношениях наследования (Inheritance), а используют отношения композиции (Composition) для повторного использования кода родительского загрузчика..

Родительская модель делегирования загрузчика классов была введена во время JDK1.2 и с тех пор широко используется почти во всех программах Java, но это не модель обязательных ограничений, а класс, рекомендованный разработчиками Java-дизайнерами.

Рабочий процесс родительской модели делегирования:Если загрузчик класса получает запрос на загрузку класса, он не будет пытаться сначала загрузить сам класс, а делегирует запрос загрузчику родительского класса для завершения Это относится к каждому уровню загрузчиков классов, поэтому все Запрос на загрузку должен в конечном итоге будет отправлен загрузчику классов запуска верхнего уровня, и только когда родительский загрузчик сообщит, что не может выполнить запрос на загрузку (необходимый класс не найден в его области поиска), дочерний загрузчик попытается выполнить загрузку самостоятельно.

Очевидным преимуществом использования родительской модели делегирования для организации отношений между загрузчиками классов является то, что класс Java имеет приоритетную иерархию со своими загрузчиками классов. Например, класс java.lang.Object хранится в rt.jar. Независимо от того, какой загрузчик классов хочет загрузить этот класс, в конечном итоге он делегируется для загрузки загрузчику запускаемого класса в верхней части модели. класс загружается в программу Один и тот же класс присутствует в различных средах загрузчика классов . Наоборот, если родительская модель делегирования не используется, и каждый загрузчик классов загружает ее сам, если пользователь напишет класс с именем java.lang.Object и поместит его в ClassPath программы, будет много разных Класс объекта, самое основное поведение в системе типов Java не гарантируется, и приложение станет хаотичным.

Родители делегируют преимущество

  • Системный класс предотвращает появление в памяти нескольких копий одного и того же байт-кода.
  • Обеспечение безопасной и стабильной работы Java-программ

image-20200827135310660

Реализация кода родительского делегирования

public Class<?> loadClass(String name)throws ClassNotFoundException {
            return loadClass(name, false);
    }
    protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
            // 首先判断该类型是否已经被加载
            Class c = findLoadedClass(name);
            if (c == null) {
                //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
                try {
                    if (parent != null) {
                         //如果存在父类加载器,就委派给父类加载器加载
                        c = parent.loadClass(name, false);
                    } else {
                    //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                        c = findBootstrapClass0(name);
                    }
                } catch (ClassNotFoundException e) {
                 // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
  

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

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

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

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

Ядром пользовательского загрузчика классов является получение файла байт-кода. Если это зашифрованный байт-код, файл необходимо расшифровать в этом классе. Поскольку это всего лишь демонстрация, я не шифровал файл класса, поэтому процесса расшифровки нет.

public class MyClassLoader extends ClassLoader {

    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("D:\\temp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.pdai.jvm.classloader.Test2");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}
  

Несколько вещей, которые следует отметить здесь :

  1. Передаваемое здесь имя файла должно быть полным именем класса, т.е.com.pdai.jvm.classloader.Test2формат, так как метод defineClass обрабатывается в этом формате.
  2. Лучше не переопределять метод loadClass, так как легко нарушить шаблон родительского делегирования.
  3. Сами такие тестовые классы могут быть загружены классом AppClassLoader, поэтому мы не можем поместить com/pdai/jvm/classloader/Test2.class в путь к классам. В противном случае из-за наличия родительского механизма делегирования класс будет загружаться непосредственно AppClassLoader, а не нашим настраиваемым загрузчиком классов.