Статьи

Архитектура JVM 101: Познакомьтесь с вашей виртуальной машиной

Курс для начинающих по архитектуре виртуальной машины Java (JVM) и байт-коду Java 101

Приложения Java окружают нас, они есть на наших телефонах, планшетах и ​​компьютерах. Во многих языках программирования это означает многократную компиляцию кода для его запуска в разных ОС. Для нас, разработчиков, возможно, самая крутая вещь в Java — это то, что она разработана для того, чтобы быть независимой от платформы (как гласит старая пословица: «Пиши один раз, беги куда угодно»), поэтому нам нужно писать и компилировать наш код только один раз.

Как это возможно? Давайте углубимся в виртуальную машину Java (JVM), чтобы выяснить это.

Архитектура JVM

Это может звучать удивительно, но сама JVM ничего не знает о языке программирования Java. Вместо этого он знает, как выполнить свой собственный набор инструкций, называемый байт-кодом Java , который организован в двоичные файлы классов . Код Java компилируется командой javac в байт-код Java, который, в свою очередь, преобразуется в машинные инструкции JVM во время выполнения.

Потоки

Java спроектирован так, чтобы быть параллельным, что означает, что различные вычисления могут выполняться одновременно, запуская несколько потоков в одном процессе. Когда начинается новый процесс JVM, в JVM создается новый поток (называемый основным потоком ). Из этого основного потока код начинает выполняться, и могут быть созданы другие потоки. Реальные приложения могут иметь тысячи запущенных потоков, которые служат различным целям. Некоторые обслуживают запросы пользователей, другие выполняют асинхронные бэкэнд-задачи и т. Д.

Стек и Рамки

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

Структура процесса JVM

Игровой площадкой JVM для выполнения метода является фрейм метода. Рамка состоит из двух основных частей:

  1. Массив локальных переменных — где хранятся параметры метода и локальные переменные
  2. Стек операндов — где выполняются вычисления метода

Рамная конструкция

Почти каждая команда байт-кода манипулирует по крайней мере одним из этих двух. Посмотрим как.

Как это работает

Давайте рассмотрим простой пример, чтобы понять, как различные элементы играют вместе для запуска нашей программы. Предположим, у нас есть эта простая программа, которая вычисляет значение 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, являются их собственными.