«Эта статья участвовала в мероприятии Haowen Convocation Order, щелкните, чтобы просмотреть:Двойные заявки на внутреннюю и внешнюю стороны, призовой фонд в 20 000 юаней ждет вас, чтобы бросить вызов!"
Управляемое чтение
У студентов, изучавших C/C++, был такой опыт.Независимо от того, какая функция реализована, при реализации на C/C++ будут следующие две проблемы:
- Управление памятью: использование программирования C / C ++, мы должны хорошо управлять системной памятью, если мы немного небрежны, может быть риск переполнения памяти
- Кроссплатформенность: например, мы используем C/C++ для реализации инструмента чата.Чтобы сделать инструмент доступным в нескольких операционных системах, таких как Windows, Mac OS и Linux, мы должны вызывать эти операционные системы одну за другой для сетевая коммуникационная часть.С функцией библиотеки для достижения этой стоимости очень высока
В результате большие ребята из Sun решили разработать язык Java, который использует JVM для запуска написанных им программ и позволяет JVM решать две вышеупомянутые проблемы: управление памятью и кросс-платформенную стыковку. Большие ребята надеются, что с помощью такой схемы программисты смогут больше сосредоточиться на реализации функций.
В Интернете есть множество статей, объясняющих часть управления памятью JVM, но большинство из этих статей имеют следующие две проблемы:
- Недостаточно тщательно, вызывая у вас чувство непонимания того, что вы знаете общее, но недостаточно
- Содержание действительно легко понять, но оно всегда кажется фрагментарным, а точки знаний не могут быть связаны, что дает вам ощущение незавершенности.
Поэтому сегодня, взяв за отправную точку реальный кейс, Сяок глубоко анализирует процесс обработки кейс-программы в JVM с точки зрения исходного кода JVM, давая вам более полное и последовательное ощущение.
кейс
Предположим, что задняя часть сообщества Nugget использует разработку Java, программист Nuggets начинает использовать следующий сегмент:
package com.juejin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class JueJinApplication {
public static void main(String[] args) {
SpringApplication.run(JueJinApplication.class, args);
}
}
Это классический класс запуска Spring Boot. Затем, когда мы упакуем этот класс в пакет jar, используйте следующую команду java для выполнения jar:
java -cp juejin.jar com.juejin.JueJinApplication
На данный момент, что происходит внутри JVM?
JNI
Учащиеся, которые пишут на Java, знают, что вход в выполнение программы Java является основным методом, поэтому JVM необходимо выполнить основной метод в приведенном выше пакете jar и управлять памятью программы. Во-первых, она должна найти основной метод, соответствующий в программу из jar.то есть main в класс JueJinApplication, а потом загрузить его в JVM, чтобы JVM могла автономно управлять памятью используемой main методом.
Итак, программисты компании Sun приступили к написанию логики поиска основного метода, во «Введении» я упомянул, что используя программирование на C/C++, мы должны хорошо управлять системной памятью, поэтому программисты обнаружили, что используя C++ для записи Поиск функции основного метода также сам по себе управляет памятью, что слишком хлопотно, поэтому они придумали решение: JNI.
JNI предусматривает набор контрактов для взаимодействия Java с другими языками программирования. Благодаря этому контракту мы можем реализовать двустороннее взаимодействие между Java и другими языками программирования. Например, мы можем использовать C++ для вызова методов Java, и наоборот, мы можем использовать Java для вызова функций C++. Как на картинке ниже:
Если у вас есть JNI, программа Sun может использовать функцию поиска, реализованную в Java, в случае основного метода, как показано ниже:
Вышеприведенный рисунок представляет собой схематическую диаграмму JVM, которая ищет основной метод при запуске команды Java в случае «Guide».JVM вызывает метод checkAndLoadMain, реализованный в Java, через функцию LoadMainClass, реализованную в C++, для поиска и загрузки основного метод.
Красная линия на приведенном выше рисунке подробно описывает процесс поиска и загрузки com.juejin.JueJinApplication и основной метод в процессе запуска JVM:
-
Запустите JVM через функцию JLI_Launch
-
JLI_Launch внутренне вызывает функцию ParseArguments для анализа параметров запуска.
-
Обнаружено, что параметр запуска равен -cp, а JVM устанавливает режим запуска LM_CLASS, указывая на то, что запускается указанный mainClass.
-
Вызовите функцию getStaticMethodID. Получите метод с именем CHECKANDLOADMAIN ID метода.
-
Позвоните в функцию NewPlatformString Функцию метода CheckandLoadMain, то есть название класса com.juejin.juejinaplication
-
Вызовите функцию CallStaticObjectMethod для выполнения метода checkAndLoadMain, см. желтую рамку справа на рисунке выше:
- Поскольку режим запуска — LM_CLASS, используйте SystemClassLoader для загрузки класса запуска mainClass, а именно com.juejin.JueJinApplication, и, конечно же, метода main в классе
В ходе описанного выше процесса мы обнаружили, что, поскольку checkAndLoadMain является методом Java, JVM вызывает этот метод через JNI.
Отсюда резюмируем контракт на вызов методов Java через JNI:
- Получить имя вызываемого метода Java с помощью функции GetStaticMethodID
- Выполнить вызываемый метод Java через функцию CallStaticObjectMethod
Это может помочь вам найти запись соответствующего метода при отладке исходного кода JVM..
Друзья, внимательно разглядывавшие картинку, должны были заметить, что я, кажется, что-то упустил. Да, здесь я добавлю: JVM будет следовать по разным ссылкам, чтобы завершить загрузку mainClass в соответствии с различными режимами запуска.На рисунке я рисую только ссылки двух режимов (-cp и -jar), потому что это два режимы запуска, которые мы обычно используем:
-
-cp: указать программу запуска класса запуска, эту ссылку я упоминал выше.
-
-jar: укажите программу запуска пакета jar.Эта ссылка в основном содержит следующие шаги, см. часть фиолетовой линии на рисунке выше:
- JVM обнаружила, что параметр запуска был -jar, поэтому установите режим запуска LM_JAR.
- Поскольку режим запуска — LM_JAR, найдите файл манифеста в банке, извлеките ключевое слово Main-Class в файл и найдите соответствующее имя mainClass.
- Как и при загрузке классов запуска в режиме LM_CLASS, используйте SystemClassLoader для загрузки класса запуска mainClass и его внутреннего основного метода.
Два других режима запуска — LM_SOURCE и LM_MODULE.Заинтересованные друзья могут изучить его самостоятельно~
Наша Java-программа в конечном итоге выполняется JVM, поэтому основной метод, загруженный в JVM, в конечном итоге обрабатывается и выполняется JVM.
Однако, прежде чем объяснить, как JVM выполняет основной метод, Сяок проведет анализ:
Все мы знаем, что независимо от того, запакован ли он maven или gradle, после упаковки файлы классов внутри пакета представляют собой байткоды, в то же время мы знаем такой закон:
Вышеприведенный рисунок — это закон обработки ЦП: сверху вниз в пирамиде производительность обработки ЦП постепенно снижается, то есть быстрее всего обрабатывается кэш ЦП, на втором месте — регистр, а медленнее всего — обработка диска.
Благодаря чтению и записи кеша CPU программа не может контролироваться. Следовательно, если JVM хочет эффективно выполнить программу, она должна захотеть максимально поместить программу в регистр, чтобы программа обработки CPU очень быстро.
Однако программа в нашем jar представляет собой кусок байт-кода, а все изучающие информатику знают, что в регистре хранятся машинные инструкции, то есть бинарные инструкции, поэтому JVM только преобразует байт-код программы в машинные инструкции, и, наконец, для того, чтобы поместить машинную инструкцию, соответствующую программе, в регистр.
Поэтому, как показано на рисунке выше, в случае «Guide», когда JVM использует SystemClassLoader для загрузки JueJinApplication, она выполняет работу инструкций по преобразованию байт-кода. ps: Для удобства интерпретации машинные инструкции справа от стрелки на рисунке заменены ассемблерными выражениями.
Однако здесь есть проблема: класс JueJinApplication и аннотация @SpringBootApplication в случае "Guide" совместно используются потоками, а инструкции в реестре читаются одна за другой, поэтому класс JueJinApplication и аннотация @SpringBootApplication являются написано Не подходит для входа в реестр, поэтому JVM разработала MetaSpace для хранения этих двух данных. В Интернете есть много статей о знаниях, связанных с MetaSpace и JMM, поэтому я не буду здесь вдаваться в подробности.
Элементы, связанные с выполнением основного метода в JueJinApplication, являются эксклюзивными для потоков и могут храниться в регистрах, поэтому сегодня мы в основном рассмотрим, как основной метод в JueJinApplication преобразуется в машинные инструкции?
Выполнение интерпретации шаблона
Давайте сначала посмотрим, как выглядит бассейн класса Juejinaplication:
public class com.juejin.JueJinApplication {
public com.juejin.JueJinApplication();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // class com/juejin/JueJinApplication
2: aload_0
3: invokestatic #3 // Method org/springframework/boot/SpringApplication.run:(Ljava/lang/Class;[Ljava/lang/String;)Lorg/springframework/context/ConfigurableApplicationContext;
6: pop
7: return
}
Здесь я кратко разбираюсь со структурой внутри.Код в коде представляет собой байт-код:
-
Байт-код в классе JueJinApplication:
- aload_0: поместите эту ссылку на вершину стека
- invokespecial #1: вызвать конструктор родительского класса JueJinApplication java.lang.Object
-
Байт-код в основном методе:
- ldc #2: поместите класс JueJinApplication на вершину стека
- aload_0: параметры в аргументах стека
- invokestatic #3: Вызвать статический метод SpringApplication.run, входными параметрами метода являются класс JueJinApplication и аргументы, а возвращаемой структурой является ConfigurableApplicationContext
- pop: всплывающее окно с возвращаемым значением метода SpringApplication.run, поскольку возвращаемое значение SpringApplication.run не используется в основном методе.
Зная байт-код в классе JueJinApplication, если мы хотим преобразовать эти инструкции байт-кода в соответствующие машинные инструкции, мы должны учитывать предпосылку:Форматы машинных инструкций, соответствующие наборам инструкций различных архитектур ЦП, различаются. Например, есть набор инструкций x86, набор инструкций ARM и т. д., и форматы машинных инструкций у них разные.. Поэтому JVM разработала такую схему для реализации преобразования инструкций байт-кода основного метода и машинных инструкций в классе JueJinApplication:
-
Структура Bytecodes определяет все байт-коды, которые будут использоваться в Java, и JVM передает эти байт-коды в TemplateTable. Как показано в верхней части рисунка выше, aload_0 и pop — это инструкции байт-кода в классе JueJinApplication.
-
Шаг TEMPLATELABLE Использование полного количества полученного базового кода, Bytecode, соответствующий генерируемому шаблону, который определяет отображение между байтомным кодом и шаблонами инструкции машин. Я здесь, чтобы посмотреть Aload_0 Bytecode Инструкции, например шаблон:
-
aload_0 => ubcp|__|clvm|__, vtos, atos, aload_0, _
, где => представляет сопоставление между инструкцией байт-кода aload_0 и соответствующим шаблоном машинной инструкции:-
=> aload_0 слева представляет инструкцию байт-кода aload_0
-
=> Правая сторона представляет шаблон машинной инструкции, соответствующий инструкции байт-кода aload_0, которая содержит 5 параметров:
-
flags: в нем определены 4 флага:
- ubcp: Использовать ли указатель байт-кода для указания на инструкцию байт-кода. Если метод в файле класса является методом Java, то инструкция байт-кода в методе нуждается в этом указателе. В это время флаг равен true. Если метод в classfile является нативным методом, так как нативный метод реализован на C/C++, достаточно вызвать метод напрямую без указателя
- disp: следует ли пересылать в рамках шаблона, например, инструкция goto перейдет к другим позициям инструкций, тогда флаг будет истинным
- CLVM: Нет необходимости вызывать функцию VM_CALL, потому что функция VM_CALL настраивается внутри ALOAD_0, поэтому CLVM в любом случае имеет значение True для False
- iswd: Является ли это широкой инструкцией. Например, инструкция байт-кода iload является широкой инструкцией, что означает чтение переменных из таблицы локальных переменных и помещение их на вершину стека. Когда таблица локальных переменных может содержать 256 переменных, то есть 2^8, это Когда iswd ложно, и инструкция iload может читать много локальных переменных, которые превысят 2^8, в это время необходимо увеличить размер таблицы локальных переменных до 2^16 , который может вместить 65536 переменных. iswd в это время верно
Согласно определению флагов, инструкция байт-кода aload_0 является методом Java, поэтому ubcp имеет значение true,
-
aload_0: указывает, что инструкция байт-кода aload_0 использует функцию aload_0 для генерации соответствующей машинной инструкции, поскольку инструкция байт-кода aload_0 соответствует более чем одной машинной инструкции.
-
vtos: входной параметр инструкции байт-кода aload_0, который является адресом входа операнда машинной инструкции, соответствующего выполнению инструкции байт-кода aload_0, которая подробно описана в разделе «Кэш вершины стека» ниже.
-
atos: выходной параметр инструкции байт-кода aload_0, который может использоваться как входной параметр следующей инструкции.
-
_
: Локальная переменная, используемая инструкцией Bytecode Aload_0. Поскольку входной параметр Aload_0 - переменная входного параметра в стеке, это не локальная переменная. Следовательно, этот параметр установлен на __
-
-
Затем JVM передает отношение сопоставления между байт-кодом и шаблоном машинных инструкций в TemplateInterpreterGenerator.
-
-
TemplateInterpreterGenerator вызывает различные ассемблеры архитектуры ЦП для генерации машинных инструкций, соответствующих инструкциям байт-кода.Я все еще использую инструкцию байт-кода aload_0 в качестве примера:
-
Предположим, JVM вызывает ассемблер архитектуры x86 для генерации машинных инструкций, то есть Ассемблер x86 (ассемблер) на рисунке выше:
- Как показано на рисунке выше, aload_0 слева в синем поле внизу — это параметр aload_0 в шаблоне на шаге 2, указывающий, что инструкция байт-кода aload_0 использует этот параметр для генерации соответствующей машинной инструкции.
- Как показано выше, машинная инструкция aload_0 справа от синего прямоугольника внизу представляет машинную инструкцию, соответствующую инструкции байт-кода aload_0.
следовательно,
aload_0 => aload_0机器指令
Он представляет собой определенный процесс ALOAD_0 BYTECODE INCURITION GROUCE MACHILE.
-
-
TemplateInterpreterGenerator сопоставляет параметр aload_0 в ассемблере x86 на шаге 3 в соответствии с шаблоном машинной инструкции aload_0, полученным на шаге 2. Два красных aload_0 на рисунке указывают на это совпадение Затем вызовите этот параметр для выполнения и сгенерируйте машинную инструкцию, соответствующую aload_0 . Инструкция aload_0 в желтом поле на приведенном выше рисунке представляет машинную инструкцию, соответствующую инструкции байт-кода aload_0.
-
Запишите сгенерированную машинную инструкцию aload_0 в ICache, кэш инструкций
-
Точно так же, как и инструкция байт-кода aload_0, JVM преобразует другие инструкции байт-кода в основном методе класса JueJinApplication для создания соответствующих машинных инструкций и записывает их как ICache.
JVM напрямую генерирует машинные инструкции через приведенный выше генератор интерпретации шаблонов TemplateInterpreterGenerator, а затем способ выполнения машинных инструкций называется выполнением интерпретации шаблона. Это форма JVM, выполняющая Java-программы, и в Hotspot есть два режима выполнения: выполнение интерпретации байт-кода и выполнение интерпретации C++. Заинтересованные студенты могут узнать об этом самостоятельно.
кэш верхней части стека
Ранее я упоминал, что целью JVM, преобразующей байт-коды в машинные инструкции, является запись преобразованных инструкций в регистры для повышения производительности программы обработки ЦП. кеш стека. Давайте возьмем инструкцию байт-кода aload_0 в основном методе в качестве примера, чтобы увидеть, как JVM кэширует вершину стека.
Написание стека верхний кеш
JVM записывает преобразованную машинную инструкцию в регистр после генерации машинной инструкции.На приведенном выше рисунке показан процесс записи байт-кода инструкции aload_0 основного метода в случае "Guide":
-
После синтаксического анализа classfile мы знаем, что входным параметром основного метода является args, поэтому мы помещаем args на вершину стека. Как показано пунктирной линией на приведенном выше рисунке.
-
Кэш верхней части стека определяет 10 состояний, которые представляют типы переменных кеша, как показано в зеленой рамке на рисунке выше.Позвольте мне объяснить:
- btos: кэшировать переменную типа bool, соответствующую bep, адресу переменной в стеке
- ztos: кэшировать переменную типа byte, соответствующую bep, адресу переменной в стеке
- ctos: кэшировать переменную типа char, соответствующую cep, адрес переменной в стеке
- stos: кэшировать переменную типа short, соответствующую sep, адресу переменной в стеке
- itos: кэшировать переменную типа int, соответствующую iep, адресу переменной в стеке
- ltos: Кэшировать переменную типа long, соответствующую lep представлению, адрес переменной в стеке
- ftos: кэш-переменные типа float, соответствующие fep, адрес переменной в стеке
- dtos: Кэшировать переменную типа double, соответствующую dep, адресу переменной в стеке
- atos: кэширует переменные объектного типа, соответствующие представлению aep, адрес переменной в стеке
- VTOS: Это очень особенное, указывая на то, что переменные / параметры, необходимые инструкцией, уже находятся в верхней части стека, нет необходимости кэшировать, соответствующий VEP, адрес переменной в стеке
До и после выполнения инструкции изменения операндов в стеке отражаются в
*ep
в этой переменной. Эти*ep
Сформируйте запись массива, как показано в зеленой части рисунка выше.Зачем использовать массив, ведь состояние до и после выполнения инструкции отражается в стеке через несколько переменных ep.Поскольку 0 в инструкции aload_0 означает выборку переменной с вершины стека, это означает, что переменная уже находится на вершине стека. соответствующий инструкции aload_0 — это адрес вершины стека. Как показано выше, vep в массиве записей указывает на вершину стека. Поскольку у инструкции aload_0 нет других операндов, все остальные переменные ep указывают на вершину стека.
-
записать каждую переменную ep в 2D-массив,Нижний индекс массива
[栈顶缓存状态][字节码指令]
, этот двумерный массив является вершиной кеша стека. Как показано на рисунке выше, запись представляет собой этот двумерный массив, и JVM записывает каждую переменную ep в массив записей, то есть позицию операнда инструкции загрузки в стеке.[vtos][aload_0],[atos][aload_0]
и Т. Д. Это завершает кеш верхней части стека.
чтение верхнего буфера стека
С кешем вершины стека JVM может найти соответствующий операнд из кеша вершины стека в соответствии с инструкцией + операндом при выполнении машинной инструкции, соответствующей основному методу, Наконец, он передается в ЦП для выполнения инструкции, используя слово aload_0 основного метода в случае.Принимая инструкцию кода раздела в качестве примера, конкретный процесс выглядит следующим образом:
Обращаем внимание на красные линии частей рисунка:
- JVM находит двумерный массив из верхней части кэша стека в
[vtos][aload_0]
, в этом блоке хранится расположение стека, соответствующего vep: вершина стека - Поскольку vtos соответствует vep, указывающему на вершину стека, JVM получает значение входного параметра args с вершины стека.
- Передать значение аргументов ЦП
- ЦП извлекает машинную инструкцию, соответствующую aload_0, из ICache.
- CPU выполняет машинную инструкцию (инструкция aload_0 + значение операнда args)
Суммировать
В этой статье я в основном объяснил концепции JNI, интерпретацию и выполнение шаблонов, а также кэширование поверх стека. Я полагаю, что у вас все еще могут быть некоторые связанные вопросы, такие как:
- Как и когда генерируется стек?
- Какие данные хранятся в стеке, двоичные или шестнадцатеричные, или они связаны с типом данных?
- Как JVM работает со стеком?
Все это хорошие вопросы, и Сяо К. подробно объяснит их в следующих статьях.
Наконец, Сяо К. все еще надеется, что друзья Наггетс смогут вдохновлять, приобретать и расти благодаря изучению статьи. Конечно, если у вас есть какие-либо вопросы, вы можете оставить сообщение в области комментариев, я верю, что каждый маленький друг в будущем станет техническим экспертом!