Механизм выполнения байт-кода виртуальной машины

Java задняя часть JVM переводчик

Так называемый «механизм выполнения байт-кода виртуальной машины» на самом деле представляет собой механизм выполнения, основанный на интерпретаторе стека JVM в соответствии с инструкциями байт-кода, приведенными в файле класса. С точки зрения непрофессионала, это процесс, в котором JVM анализирует инструкции байт-кода и выводит результат операции. Далее давайте подробно рассмотрим эту часть.

Вызов метода

Прежде чем описывать «движок исполнения байт-кода», давайте посмотрим, как выглядит вызов метода на основе кадра стека с уровня сборки. (В качестве примера возьмем набор инструкций процессора IA32)

Программа IA32 использует структуру данных кадра стека для поддержки вызовов процедур (называемых методами на языке Java), каждая процедура соответствует кадру стека, а вызов процедуры соответствует размещению и извлечению кадра стека. В определенный момент доступен только кадр стека наверху стека, который представляет различные состояния выполняемого метода. Самый верхний фрейм стека ограничен двумя указателями: указателем стека и указателем фрейма. Они соответствуют адресам в стеке и соответственно хранятся в регистрах%ebpа также%espсередина. Общая структура стека выглядит следующим образом:

image

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

Давайте посмотрим на кусок кода C:

#include<stdio.h>
void sayHello(int age)
{
    int x = 32;
    int y = 2323;
    age = x + y;
}

void main()
{
    int age = 22;
    sayHello(age);
}

Очень простой кусок кода, собираем для генерации соответствующего ассемблерного кода, опуская часть кода ссылки, оставляя основную часть:

main:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$20, %esp
	movl	$22, -4(%ebp)
	movl	-4(%ebp), %eax
	movl	%eax, (%esp)
	call	sayHello
	leave
	ret
	
sayHello:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$16, %esp
	movl	$32, -4(%ebp)
	movl	$2323, -8(%ebp)
	movl	-8(%ebp), %eax
	movl	-4(%ebp), %edx
	addl	%edx, %eax
	movl	%eax, -12(%ebp)
	leave
	ret

Сначала посмотрите на ассемблерный код основной функции, первые два компилятора в основной функции и первые две инструкции в Sayhello одинаковы, и мы вводим их в последнюю.

Инструкция subl вычитает 20 из адреса в регистре %esp, то есть указатель стека расширяется вверх на 20 байт (стек растет в обратном направлении), то есть 20 байт отводится под текущий кадр стека. Далее movl записывает значение 20 по адресу-4(%ebp), этот адрес на самом деле на четыре байта выше позиции относительного указателя кадра регистра %ebp. Если значение %ebp равно: 0x14, то 20 сохраняется в адресе стека по адресу 0x10.

Затем инструкция movl извлекает значение параметра age и сохраняет его в регистре %eax.

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

Решение здесь состоит в том, что первым шагом инструкции call являетсяОбратный адресНажмите, а затем перейдите к реализации подхода sayHell, где мы не можем видеть, что процесс толчка интегрируется в одну инструкцию.

Затем перейдите к первой инструкции метода sayHello, чтобы начать выполнение, pushl помещает адрес в регистре %ebp в стек, в это время %ebp является адресом указателя кадра предыдущего кадра стека, эта операция фактически является действием сохранения. . Затем инструкция movl указывает указатель фрейма на позицию указателя стека, которая является вершиной стека, а затем расширяет указатель стека вверх на 16 байт.

Далее записываем значения 32 и 2323 в разные адреса стека, которые можно вычислить относительно адреса указателя фрейма.

Следующая операция заключается в записи x и y в регистры %eax и %edx соответственно, а затем добавлении инструкции для выполнения сложения и сохранения в регистре %eax. Затем поместите результат в стек.

Инструкция leave эквивалентна сумме следующих двух инструкций:

movl %ebp %esp
popl %ebp

Что это значит?

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

Инструкция ret используется для восстановления состояния перед вызовом и продолжения выполнения основного метода.

Вызовы методов всего IA32 в основном такие же, как и выше.Для 64-битной x86-64 добавлено 16 регистров, и регистры предпочтительно используются для вычисления и передачи параметров, что повышает эффективность. Но недостатком этого метода хранения на основе стека является «плохая переносимость», и использование регистров на разных машинах должно быть разным. Так что наша Java, несомненно, использует стек.

Структура кадра стека времени выполнения

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

таблица локальных переменных

Таблица локальных переменных используется для хранения различных переменных, используемых при выполнении метода, а также параметров метода. В спецификации виртуальной машины указано, что емкость таблицы локальных переменных использует переменный слот (слот) как наименьшую единицу, но не указывает фактический размер слота, а только то, что каждый слот должен иметь возможность хранить любое логическое значение, байт , char, short, int, float, ссылка и т. д.

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

Таблица локальных переменных отличается от стека операндов, она использует механизм индексации для доступа к элементам и отличается от способа помещения стека операндов в стек и из него. Например:

public void sayHello(String name){
        int x = 23;
        int y = 43;
        x++;
        x = y - 2;
        long z = 234;
        x = (int)z;
        String str = new String("hello wrold ");
    }

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

image

Как видите, первая запись в таблице локальных переменных — это ссылка на класс с именем this, которая указывает на ссылку на текущий объект в куче. Далее идут параметры нашего метода, локальные переменные x, y, z и str.

На самом деле это косвенно показывает, что каждый из наших методов экземпляра по умолчанию передается в параметре this, указывающем на ссылку экземпляра текущего класса.

стек операндов

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

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

обратный адрес

После того, как метод завершает вызов другого метода, ему необходимо вернуться в место вызова, чтобы продолжить выполнение следующего тела метода. Затем место, где вызываются другие методы, называется «обратным адресом».Нам нужно убедиться, что ЦП может вернуться в исходное место вызова после выполнения других методов, а затем продолжить тело метода вызывающей стороны.

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

вызов метода

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

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

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

Например следующий код:

Object obj = new String("hello");
obj.equals("world");

В классе Object есть метод equals, а в классе String есть метод equals.Приведенная выше программа, очевидно, вызывает метод equals класса String. Затем, если мы загрузим класс Object и укажем ссылку на символ равенства непосредственно на прямую ссылку его собственного метода equals, то указанный выше объект всегда будет вызывать метод equals объекта. Тогда наш полиморфизм никогда не будет реализован.

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

Все остальные методы вместе называются «виртуальными методами» и не будут разрешены на этапе синтаксического анализа загрузки класса. Проблем с вызовом этих методов нет. Виртуальная машина может найти запись метода непосредственно по прямой ссылке, но «невиртуальный метод» отличается. Виртуальной машине нужна определенная стратегия, чтобы найти фактический метод. Давайте посмотрим на это вместе.

статическая диспетчеризация

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

public class Father {
}
public class Son extends Father {
}
public class Daughter extends Father {
}
public class Hello {
    public void sayHello(Father father){
        System.out.println("hello , i am the father");
    }
    public void sayHello(Daughter daughter){
        System.out.println("hello i am the daughter");
    }
    public void sayHello(Son son){
        System.out.println("hello i am the son");
    }
}
public static void main(String[] args){
    Father son = new Son();
    Father daughter = new Daughter();
    Hello hello = new Hello();
    hello.sayHello(son);
    hello.sayHello(daughter);
}

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

hello , i am the father

hello , i am the father

Я не знаю, правильно ли вы поняли? Это очень распространенный вопрос на собеседовании.Проверкой является ваше понимание логики перегрузки методов и диспетчеризации методов. Давайте проанализируем это:

Прежде всего, нам нужно ввести два понятия: «статический тип» и «фактический тип». Статический тип относится к типу, обернутому в самый внешний слой переменной.Например, упомянутый выше Отец является так называемым статическим типом, а Сын или Дочь — фактическим типом.

Когда наш компилятор генерирует инструкции байт-кода, онстатический типВыберите способ вызова соответствующего метода. Что касается примеров, описанных выше:

image

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

(LStaticDispathch/Father;)V

то есть

public void sayHello(Father father){}

Все действия диспетчеризации, которые полагаются на статические типы для поиска исполняемой версии метода, называются «статической диспетчеризацией», а перегрузка метода является типичным проявлением статической диспетчеризации. Но следует отметить, что статическая диспетчеризация не имеет значения, каков ваш фактический тип, она только вызывает вызовы методов на основе вашего статического типа.

Динамическая отправка

public class Father {
    public void sayHello(){
        System.out.println("hello world ---- father");
    }
}
public class Son extends Father {
    @Override
    public void sayHello(){
        System.out.println("hello world ---- son");
    }
}
public static void main(String[] args){
    Father son = new Son();
    son.sayHello();
}

Выходной результат:

hello world ---- son

Очевидно, что в конце концов вызывается метод sayHello подкласса Давайте посмотрим на сгенерированный вызов инструкции байт-кода:

image

image

вы видели? Инструкция вызова метода, сгенерированная для нас компилятором, выбирает вызов соответствующего метода статического типа, но почему конечный результат вызывает соответствующий метод фактического типа?

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

  • Извлеките верхний элемент стека операндов, определите его фактический тип и обозначьте его как C
  • Находит метод типа C с тем же простым именем и дескриптором, что и вызываемый метод, и возвращает прямую ссылку на метод, если он есть.
  • В противном случае выполните поиск в родительском классе C и верните прямую ссылку на метод
  • В противном случае сгенерируйте исключение java.lang.AbstractMethodError

Таким образом, здесь у нас есть пример вызова метода - это самоочевидное сын Seheello Subclass.

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

Поддержка атрибутов динамического типа

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

Object obj = new String("hello-world");
obj.split("-");

В Java две строчки кода не могут пройти компилятор, причина в том, что компилятор проверяет, что статический тип переменной obj — Object, а в классе Object нет метода subString, поэтому сообщается об ошибке.

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

Статические языки проверяют типы переменных во время компиляции и обеспечивают строгие проверки, в то время как динамические языки проверяют фактические типы переменных во время выполнения, придавая программам большую гибкость. У каждого есть свои преимущества и недостатки.Преимущество статических языков — безопасность, а недостаток — отсутствие гибкости.Динамические языки наоборот.

JDK1.7 предоставляет два способа поддержки динамических функций Java: директиву invokedynamic и пакет java.lang.invoke. Реализация обоих аналогична, и мы вводим только основное содержание последнего.

//该方法是我自定义的,并非 invoke 包中的
public static MethodHandle getSubStringMethod(Object obj) throws NoSuchMethodException, IllegalAccessException {
    //定义了一个方法模板,规定了待搜索的方法的返回值和参数类型
    MethodType methodType = MethodType.methodType(String[].class,String.class);
    //查找符合指定方法简单名称和模板信息的方法
    return lookup().findVirtual(obj.getClass(),"split",methodType).bindTo(obj);
}
public static void main(String[] args){
    Object obj = new String("hello-world");
    //定位方法,并传入参数执行方法
    String[] strs = (String[]) getSubStringMethod(obj).invokeExact("-");
    System.out.println(strs[0]);
}

Выходной результат:

hello

Видите ли, хотя статический тип нашего obj — Object, таким образом я могу обойти проверку типов компилятора и напрямую выполнить метод, указанный мной во время выполнения.

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

Чтобы обобщить интерпретацию метода VM VM Hotspot на основе стека операнда, все промежуточные результаты операции и параметров метода и т. Д. Наряду с базовой эксплуатацией стека, вынесенного или сохраненного. Самое большое преимущество этого механизма заключается в том, чтоПортативность. В отличие от механизма выполнения метода на основе регистров, он слишком сильно зависит от базового оборудования и не может быть легко кросс-платформенным, но недостатки также очевидны.То есть для выполнения одной и той же операции требуется относительно больше инструкций.


Весь код, изображения, файлы в статье хранятся в облаке на моем GitHub:

(https://github.com/SingleYam/overview_java)

Добро пожаловать в публичный аккаунт WeChat: Gorky on the code, все статьи будут синхронизированы в публичном аккаунте.

image