Курс для начинающих по архитектуре виртуальной машины Java (JVM) и байт-коду Java 101
Приложения Java окружают нас, они есть на наших телефонах, планшетах и компьютерах. Во многих языках программирования это означает многократную компиляцию кода для его запуска в разных ОС. Для нас, разработчиков, возможно, самая крутая вещь в Java — это то, что она разработана для того, чтобы быть независимой от платформы (как гласит старая пословица: «Пиши один раз, беги куда угодно»), поэтому нам нужно писать и компилировать наш код только один раз.
Как это возможно? Давайте углубимся в виртуальную машину Java (JVM), чтобы выяснить это.
Архитектура JVM
Это может звучать удивительно, но сама JVM ничего не знает о языке программирования Java. Вместо этого он знает, как выполнить свой собственный набор инструкций, называемый байт-кодом Java , который организован в двоичные файлы классов . Код Java компилируется командой javac в байт-код Java, который, в свою очередь, преобразуется в машинные инструкции JVM во время выполнения.
Потоки
Java спроектирован так, чтобы быть параллельным, что означает, что различные вычисления могут выполняться одновременно, запуская несколько потоков в одном процессе. Когда начинается новый процесс JVM, в JVM создается новый поток (называемый основным потоком ). Из этого основного потока код начинает выполняться, и могут быть созданы другие потоки. Реальные приложения могут иметь тысячи запущенных потоков, которые служат различным целям. Некоторые обслуживают запросы пользователей, другие выполняют асинхронные бэкэнд-задачи и т. Д.
Стек и Рамки
Каждый поток Java создается вместе со стеком фреймов, предназначенным для хранения фреймов метода, а также для управления вызовом и возвратом метода. Фрейм метода используется для хранения данных и частичных вычислений метода, которому он принадлежит. Когда метод возвращается, его кадр отбрасывается. Затем его возвращаемое значение передается обратно в фрейм invoker, который теперь может использовать его для выполнения своих собственных вычислений.
Игровой площадкой JVM для выполнения метода является фрейм метода. Рамка состоит из двух основных частей:
- Массив локальных переменных — где хранятся параметры метода и локальные переменные
- Стек операндов — где выполняются вычисления метода
Почти каждая команда байт-кода манипулирует по крайней мере одним из этих двух. Посмотрим как.
Как это работает
Давайте рассмотрим простой пример, чтобы понять, как различные элементы играют вместе для запуска нашей программы. Предположим, у нас есть эта простая программа, которая вычисляет значение 2 + 3 и печатает результат:
01
02
03
04
05
06
07
08
09
10
|
class SimpleExample { public static void main(String[] args) { int result = add( 2 , 3 ); System.out.println(result); } public static int add( int a, int b) { return a+b; } } |
Чтобы скомпилировать этот класс, мы запускаем javac SimpleExample.java , что приводит к скомпилированному файлу SimpleExample.class . Мы уже знаем, что это двоичный файл, который содержит байт-код. Итак, как мы можем проверить байт-код класса? Используя javap .
javap — это инструмент командной строки, который поставляется с JDK и может разбирать файлы классов. Вызов javap -c -p выводит дизассемблированный байт-код (-c) класса, включая закрытые (-p) члены и методы:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
Compiled from "SimpleExample.java" class SimpleExample { SimpleExample(); Code: 0 : aload_0 1 : invokespecial # 1 // Method java/lang/Object."":()V 4 : return public static void main(java.lang.String[]); Code: 0 : iconst_2 1 : iconst_3 2 : invokestatic # 2 // Method add:(II)I 5 : istore_1 6 : getstatic # 3 // Field java/lang/System.out:Ljava/io/PrintStream; 9 : iload_1 10 : invokevirtual # 4 // Method java/io/PrintStream.println:(I)V 13 : return public static int add( int , int ); Code: 0 : iload_0 1 : iload_1 2 : iadd 3 : ireturn } |
Что происходит внутри JVM во время выполнения? java SimpleExample запускает новый процесс JVM, и создается основной поток. Для основного метода создается новый кадр, который помещается в стек потоков.
01
02
03
04
05
06
07
08
09
10
|
public static void main(java.lang.String[]); Code: 0 : iconst_2 1 : iconst_3 2 : invokestatic # 2 // Method add:(II)I 5 : istore_1 6 : getstatic # 3 // Field java/lang/System.out:Ljava/io/PrintStream; 9 : iload_1 10 : invokevirtual # 4 // Method java/io/PrintStream.println:(I)V 13 : return |
Основной метод имеет две переменные: аргументы и результат . Оба находятся в таблице локальных переменных. Первые две команды байт-кода main, iconst_2 и iconst_3 загружают постоянные значения 2 и 3 (соответственно) в стек операндов. Следующая команда invokestatic вызывает статический метод add. Поскольку этот метод ожидает два целых числа в качестве аргументов, invokestatic извлекает два элемента из стека операндов и передает их в новый фрейм, созданный JVM для добавления . На этом этапе стек операндов main пуст.
1
2
3
4
5
6
|
public static int add( int , int ); Code: 0 : iload_0 1 : iload_1 2 : iadd 3 : ireturn |
В кадре добавления эти аргументы хранятся в массиве локальных переменных. Первые две команды байт-кода iload_0 и iload_1 загружают 0-ю и 1-ю локальные переменные в стек. Затем iadd извлекает два верхних элемента из стека операндов, суммирует их и помещает результат обратно в стек. Наконец, ireturn выводит верхний элемент и передает его в вызывающий фрейм как возвращаемое значение метода, и фрейм отбрасывается.
01
02
03
04
05
06
07
08
09
10
|
public static void main(java.lang.String[]); Code: 0 : iconst_2 1 : iconst_3 2 : invokestatic # 2 // Method add:(II)I 5 : istore_1 6 : getstatic # 3 // Field java/lang/System.out:Ljava/io/PrintStream; 9 : iload_1 10 : invokevirtual # 4 // Method java/io/PrintStream.println:(I)V 13 : return |
В стеке main теперь содержится возвращаемое значение add . istore_1 извлекает его и устанавливает в качестве значения переменной с индексом 1, что является результатом . getstatic помещает в стек статическое поле java / lang / System.out типа java / io / PrintStream . iload_1 помещает переменную с индексом 1, который является значением результата, который теперь равен 5, в стек. Таким образом, в этот момент стек содержит 2 значения: поле ‘out’ и значение 5. Теперь invokevirtual собирается вызвать метод PrintStream.println . Он извлекает два элемента из стека: первый — это ссылка на объект, для которого будет вызван метод println. Второй элемент — это целочисленный аргумент, передаваемый методу println, который ожидает один аргумент. Здесь основной метод печатает результат добавления . Наконец, команда return завершает метод. Основной кадр отбрасывается, и процесс JVM заканчивается.
Это оно. В общем, не слишком сложно.
«Пиши один раз, беги куда угодно»
Так что же делает Java независимой от платформы? Все это лежит в байт-коде.
Как мы видели, любая Java-программа компилируется в стандартный Java-байт-код. Затем JVM преобразует его в конкретные машинные инструкции во время выполнения. Нам больше не нужно проверять совместимость нашего кода с компьютером. Вместо этого наше приложение может работать на любом устройстве, оборудованном JVM, и JVM сделает это за нас. Работа сопровождающих JVM заключается в предоставлении различных версий JVM для поддержки различных машин и операционных систем.
Эта архитектура позволяет любой программе Java работать на любом устройстве, на котором установлена JVM. И так происходит волшебство.
Последние мысли
Разработчики Java могут писать отличные приложения, не понимая, как работает JVM. Однако изучение архитектуры JVM, изучение ее структуры и понимание того, как она интерпретирует ваш код, поможет вам стать лучшим разработчиком. Это также поможет вам время от времени решать действительно сложные проблемы
PS. Если вы ищете более глубокое погружение в JVM и как все это относится к исключениям Java, не смотрите дальше! ( Здесь все в порядке. )
См. Оригинальную статью здесь: Архитектура JVM 101: Познакомьтесь с вашей виртуальной машиной
Мнения, высказанные участниками Java Code Geeks, являются их собственными. |