English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Полный процесс загрузки Java-класса
Java-файл, начиная с загрузки и заканчивая выгрузкой, проходит через четыре этапа:
Загрузка -> linkage (валидация + подготовка + разрешение) -> инициализация (подготовка перед использованием) -> использование -> выгрузка
Процесс загрузки (кроме пользовательской загрузки) и linkage полностью контролируется JVM. Когда необходимо выполнять инициализацию класса (загрузка и linkage уже завершены), JVM имеет строгие правила (четыре случая):
1. При встрече с инструкциями bytecode new, getstatic, putstatic, invokestatic, если класс еще не был инициализирован, то его необходимо немедленно инициализировать. Это три основных случая: инстанцирование класса с помощью new, чтение или настройка статических полей класса (не включая статические поля, помеченные final, так как они уже включены в пул констант), а также выполнение статического метода.
2. При использовании методов java.lang.reflect.* для反射ного вызова класса, если класс еще не был инициализирован, его сразу инициализируют.
3. При инициализации класса, если его родитель еще не инициализирован, сначала инициализируется его родитель.
4. При запуске JVM пользователь должен указать класс, который нужно выполнить (класс, содержащий static void main(String[] args)), и JVM сначала инициализирует этот класс.
upper 4 preprocessing is called an active reference to a class, and all other cases are called passive references, which do not trigger the initialization of the class. Below are also some examples of passive references:
/** * Пассивное использование情景 1 * Указание на статическое поле родительского класса через подкласс не вызывает инициализацию подкласса * @author volador * */ class SuperClass{ static{ System.out.println("init super class."); } public static int value=123; } class SubClass extends SuperClass{ static{ System.out.println("init sub class."); } } public class test{ public static void main(String[]args){ System.out.println(SubClass.value); } }
Результат вывода: init super class.
/** * Пассивное использование情景 2 * Указание на класс через массив не вызывает инициализацию этого класса * @author volador * */ public class test{ public static void main(String[] args){ SuperClass s_list=new SuperClass[10]; } }
Результат вывода: без вывода
/** * Пассивное использование情景 3 * Константы сохраняются в постоянный пул класса, который вызывает его, по сути, не ссылается на класс, определяющий константу, поэтому автоматически не вызывается инициализация класса, определяющего константу * @author root * */ class ConstClass{ static{ System.out.println("ConstClass init."); } public final static String value="hello"; } public class test{ public static void main(String[] args){ System.out.println(ConstClass.value); } }
Результат вывода: hello (подсказка: при компиляции, ConstClass.value был преобразован в константу 'hello' и добавлен в постоянный пул класса test)
Ниже приведены примеры инициализации класса, интерфейсы также должны быть инициализированы, инициализация интерфейса несколько отличается от инициализации класса:
Все эти коды выводят информацию о инициализации с помощью static{}, интерфейсы это сделать не могут, но компилятор все же создает для интерфейса класс конструктор <clinit>() для инициализации членов переменных интерфейса, это также делается в инициализации класса. Важно отметить, что真正 разница заключается в третьем пункте, инициализация класса требует, чтобы все родительские классы были полностью初始化ированы перед выполнением инициализации класса, но инициализация интерфейса не так требовательна к инициализации родительского интерфейса, то есть, при инициализации подинтерфейса не требуется, чтобы родительский интерфейс был полностью инициализирован, только когда realmente используется родительский интерфейс, он будет инициализирован (например, при обращении к константам интерфейса).
Разберем全过程 загрузки класса: загрузка -> проверка -> подготовка -> разрешение -> инициализация
В первую очередь это загрузка:
Этот блок виртуальной машины должен выполнить три вещи:
1. Получить двоичный поток, представляющий этот класс, через полное имя класса.
2. Преобразовать статическую структуру хранения, представленную этим байтовым потоком, в данные методной области для выполнения.
3. В java-пуле создать объект java.lang.Class, представляющий этот класс, в качестве входа для данных методной области.
Что касается первого пункта, это очень гибко, здесь切入 множество технологий, так как здесь не ограничивается来源 байтового потока:
Из файла class -> обычная загрузка файла
Из архива zip -> загрузка классов из jar
Из Интернета -> Applet
..........
Сравнительно с другими этапами процесса загрузки, загрузочный этап имеет самую высокую степень контролируемости, потому что классовый загрузчик может быть системным или собственным, программист может написать загрузчик, чтобы контролировать получение байтового потока.
После завершения получения двоичного потока он будет сохранен в методной области в соответствии с требованиями JVM, и в java-пуле будет создан объект java.lang.Class, связанный с данными в пуле.
После завершения загрузки необходимо начать проверку этих байтовых потоков (многие из этих шагов выполняются параллельно, например, проверка формата файла):
Цель проверки: обеспечить, чтобы байтовой поток информации в классе файла соответствовал вкусам JVM, чтобы JVM не чувствовал себя неуютно. Если классовый файл компилируется из чистого Java-кода, естественно, не会出现 таких проблем, как переполнение массива, переход к несуществующему блоку кода и т.д., так как при возникновении таких явлений компилятор откажется компилировать. Однако, как и было сказано ранее, поток данных Class файла не обязательно приходит из Java-источника, он может поступать из Интернета или других источников, даже можно самим написать в шестнадцатеричном формате. Если JVM не будет проверять эти данные, могут возникнуть вредоносные байтовые потоки, которые могут привести к полному сбою JVM.
Проверка проходит несколько шагов: проверка файла -> проверка метаданных -> проверка байт-кода -> проверка символических ссылок
Проверка файла: проверяет, соответствует ли поток байт стандарту файла Class и может ли текущая версия JVM обрабатывать его версию. После успешной проверки поток байт можно сохранить в методную область памяти. Последующие три проверки выполняются в методной области.
Проверка метаданных: семантический анализ информации, описанной байт-кодом, чтобы убедиться, что она соответствует грамматическим нормам языка Java.
Проверка байт-кода: самая сложная, проверяет содержимое метода, чтобы убедиться, что во время выполнения он не будет выполнять что-то нестандартное.
Проверка символических ссылок: проверяет真实性 и возможность использования некоторых ссылок, например, если в коде используется другой класс, необходимо проверить, существует ли этот класс;或者说, если в коде доступа к свойствам другого класса, необходимо проверить возможность доступа к этим свойствам. (Этот шаг将为 дальнейшую интерпретацию заложить основу)
Этап проверки очень важен, но не является обязательным. Если некоторые коды используются повторно и проверены на надежность, на этапе выполнения можно попробовать использовать параметр -Xverify:none, чтобы отключить большинство мер проверки классов, чтобы уменьшить время загрузки классов.
После завершения предыдущего шага, мы переходим к этапу подготовки:
На этом этапе для классовых переменных (являющихся статическими переменными) выделяется память и устанавливается их начальное значение, и эта память выделяется в области метода. Следует отметить, что на этом этапе только устанавливается начальное значение для статических переменных, а переменные实例 выделяются при инстанцировании объектов. Начальное установление значения для классовых переменных несколько отличается от их присвоения, например:
public static int value=123;
На этом этапе значение value будет равно 0, а не 123, потому что в этот момент еще не начинается выполнение java-кода, 123 все еще не виден, и команда putstatic, котораяassigns 123 к value,存在于 в <clinit>() после компиляции программы, поэтому присваивание значения 123 к value выполняется только на этапе инициализации.
Здесь есть и исключение:
public static final int value=123;
Здесь на этапе подготовки значение переменной value будет инициализировано как 123. Это означает, что на этапе компиляции javac создает атрибут ConstantValue для этой специальной переменной value, и на этапе подготовки jm устанавливает значение value на основе значения ConstantValue.
После завершения предыдущего шага, нужно перейти к интерпретации. Интерпретация, кажется, преобразует поля, методы и другие элементы класса, и это связано с форматом содержимого файла Class, но не углубляется в это.
Процесс инициализации является последним шагом процесса загрузки класса: }}
В процессе загрузки класса, кроме этапа загрузки, где пользователь может участвовать через пользовательский классовый загрузчик, все другие действия полностью контролируются JVM. Когда arrives этап инициализации, начинается真正е выполнение кода java.
Этот шаг выполнит некоторые предварительные операции, обратите внимание, что в этапе подготовки уже было выполнено системное присваивание классовым переменным.
На самом деле, этот шаг представляет собой процесс выполнения метода <clinit>(); программы. Давайте рассмотрим метод <clinit>(); подробнее:
Метод <clinit>() называется методом конструктора класса и представляет собой автоматическое объединение всех действий присваивания классовых переменных и выражений в статических блоках кода, сохраняя их порядок, как они排列ены в исходном файле.
Метод <clinit>(); отличается от метода конструктора класса, он не требует явного вызова метода <clinit>(); суперкласса. JVM гарантирует, что метод <clinit>(); subclasses будет выполнен до выполнения метода <clinit>(); суперкласса, то есть, первым被执行ым <clinit>() в виртуальной машине肯定是 метод java.lang.Object.
Давайте рассмотрим пример:
static class Parent{ public static int A=1; static{ A=2; } } static class Sub extends Parent{ public static int B=A; } public static void main(String[] args){ System.out.println(Sub.B); }
Сначала в Sub.B был сделан вызов статических данных, поэтому класс Sub необходимо инициализировать. В то же время, суперкласс Parent нужно сначала инициализировать. После инициализации Parent, A=2, поэтому B=2; этот процесс эквивалентен:
static class Parent{ <clinit>(){ public static int A=1; static{ A=2; } } } static class Sub extends Parent{ <clinit>(){ // JVM сначала выполнит该方法 суперкласса, а затем выполнит эту public static int B=A; } } public static void main(String[] args){ System.out.println(Sub.B); }
Метод <clinit>(); не является обязательным для класса и интерфейса. Если в классе или интерфейсе не выполняется присваивание классовым переменным и нет статического блока кода, метод <clinit>() не будет генерироваться компилятором.
Поскольку в интерфейсе не может существовать блок статического кода static{}, но все же может выполняться операция присваивания переменных при инициализации переменных, поэтому в интерфейсе также генерируется конструктор <clinit>(). Но в отличие от класса, перед выполнением метода <clinit>(); подинтерфейса не нужно выполнять метод <clinit>(); суперинтерфейса. Инициализация суперинтерфейса происходит только тогда, когда используются переменные, определенные в суперинтерфейсе.
Кроме того, при инициализации реализующего класса интерфейса也不会 выполняться метод <clinit>() интерфейса.
Кроме того, JVM гарантирует, что метод <clinit>(); класса будет правильно заблокирован и синхронизирован в многоthreaded среде. <Потому что инициализация выполняется только один раз>.
Давайте рассмотрим пример:
public class DeadLoopClass { static{ if(true){ System.out.println("Будет инициализирован ["+Thread.currentThread()+"] с последующей бесконечной петлей"); while(treu){} } } /** * @param args */ public static void main(String[] args) { // TODO Автоматически сгенерированный метод stub System.out.println("toplaile"); Runnable run=new Runnable(){ @Override public void run() { // TODO Автоматически сгенерированный метод stub System.out.println("["+Thread.currentThread()+"] Будет инстанцирован этот класс"); DeadLoopClass d=new DeadLoopClass(); System.out.println("["+Thread.currentThread()+"] Завершена инициализация класса"); }}; new Thread(run).start(); new Thread(run).start(); } }
Здесь, во время выполнения, вы увидите блокировку.
Спасибо за чтение, надеюсь, это поможет вам, спасибо за поддержку нашего сайта!