Виртуальная машина Java — JVM

Дизайн JVM разрабатывался на основе многолетнего опыта программирования на таких языках как С и С++. Это позволило создать такую структуру JVM, которая сделала жизнь программиста значительно легче:

  • код приложения выполняется в контейнере,
  • в наличии защищённая среда выполнения программы,
  • уменьшилась до минимума возможность управления памятью программистом,
  • среду исполнения сделали кроссплатформенной,
  • стала использоваться рантайм (во время выполнения программы) информация для самоуправления.

Последний аспект позволяет JVM принимать более оптимальные решения при выполнении программы, основываясь на том как часто вызываются некоторые её блоки. Собственно виртуальная машина интерпретирует байт код скомпилированной джава программы, однако в JVM существует возможность компилировать часто вызываемые блоки программы в машинный код в рантайме. Эта технология называется Jast-in-time (JIT). Это не значит что машинный код сохранится в файл программы, он будет существовать только во время её выполнения в оперативной памяти. Таким образом производительность джава программы, после нескольких циклов работы, может стать выше чем у аналогичных программ компилируемых языков C и C++.

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

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

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

Почему его назвали байткодом?
Код инструкции (opcode код операции) это только один байт (некоторые операции имеют параметры которые следуют за байтом операции в виде потока байт), таким образом существует только 256 вариантов инструкций. На практике некоторые не используются, остаётся примерно 200 используемых, но некоторые из них не были задействованы в последней версии javac.

Является ли компилятором javac?
Обычно компилятор создаёт машинный код, а javac создаёт байткод, непохожий на машинный код. Однако class файлы немного похожи на объектные файлы (как в Windows *.dll или Unix *.so) и являются нечитабельными.

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

Таким образом в чистом виде javac не компилятор, однако в большинстве книг и статей можно увидеть такие словосочетания — компилятор исходного кода или javac компилятор. А собственно компиляцией занимается JIT, когда создаёт машинный код для оптимизации выполнения программы.

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

Является ли байткод действительно машинно независимым? Как на счёт порядка байт?
Формат байт кода всегда один и тот же, не важно на какой машине он был создан, это всегда big-endian (от старшего разряда к младшему). Например целое число занимающее в памяти 4 байта, хранится там побайтно от старших разрядов к младшим.

Является ли джава интерпретируемым языком?
По существу JVM это интерпретатор (с JIT компиляцией, которая даёт прирост производительности). Собственно исходный код джава не поступает интерпретатору на исполнение, он компилируется в байткод, а уже байт код интерпретируется JVM.

Могут ли другие языки выполнятся на JVM?
Всё что компилируется в байткод, может быть выполнено на JVM. Примеры таких языков — Scala, Clojure, Kotlin и т.д.
Кроме того, есть возможность реализовать интерпретатор некоторого языка на джава. Как это сделано, например, для языка JRuby.

Чтобы увидеть байт код, достаточно открыть файл .class в простом текстовом редакторе. Он окажется явно не читабельным. Однако у нас есть возможность дизассемблировать его в мнемоники байткода, то есть в элементарные команды (которые можно прочитать) и данные.

Рассмотрим простую программу складывающую два числа.

public class Main {
    /**
     * Метод складывающий два числа.
     * @param a первое слагаемое
     * @param b второе слагаемое
     * @return результат сложения двух чисел.
     */
    private static int adding(int a, int b) {
        return a + b;
    }
    /**
     * Точка входа в программу.
     * @param args неиспользуемые параметры командной строки.
     */
    public static void main(String[] args) {
        // первое слагаемое
        int x = 43;
        // второе слагаемое
        int y = 56;
        // сложение значений двух переменных и помещение
        // результата в переменную.
        int result = adding(x, y);
        // вывод на экран значения переменной result.
        System.out.println(result);
    }
}

Для удобства, можно создать пустой проект в Idea, и поместить этот класс в папку src.
Запустите его на исполнение. Idea скомпилирует его, естественно используя для этого javac, создаст файл Main.class и запустит его на исполнение. Проверим что у нас всё получилось, в консоли должна появится сумма чисел.

Затем перейдём в терминале в папку вашего проекта, а затем в папку где должен быть создан файл class. Это должно быть в папке out/production.

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

javap -c -p Main.class

-с — дизассемблирование,
-p — вывод информации о всех членах класса.

В терминале мы должны получить следующее:

public class org.dart.Main { 
  public org.dart.Main(); 
    Code: 
       0: aload_0 
       1: invokespecial #1   // Method java/lang/Object."<init>":()V 
       4: return 

  private static int adding(int, int); 
    Code: 
       0: iload_0 
       1: iload_1 
       2: iadd 
       3: ireturn 

  public static void main(java.lang.String[]); 
    Code: 
       0: bipush        43   // литерал числа присвоенный первой переменной
       2: istore_1 
       3: bipush        56   // литерал числа присвоенный второй переменной
       5: istore_2 
       6: iload_1 
       7: iload_2 
       8: invokestatic  #2   // Method adding:(II)I 
      11: istore_3 
      12: getstatic     #3   // Field java/lang/System.out:Ljava/io/PrintStream; 
      15: iload_3 
      16: invokevirtual #4   // Method java/io/PrintStream.println:(I)V 
      19: return 
} 

Здесь можно увидеть много интересного, даже код который мы явно сами не писали. Например метод:

public org.dart.Main(); 
    Code: 
       0: aload_0 
       1: invokespecial #1   // Method java/lang/Object."<init>":()V 
       4: return

Это не обычный метод, это конструктор. Он добавился автоматически при компиляции. Почему он добавляется это отдельная история.

Обратите внимание, кажется что нумерация строк кода внутри методов идёт не по порядку. Это не совсем так. Она учитывает длину параметров от которых зависят некоторые операции.

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

private static int adding(int, int); 
    Code: 
       0: iload_0    // загрузка первого целого числа из параметра в стек
       1: iload_1    // загрузка второго целого числа из параметра в стек
       2: iadd       // сложение двух чисел из стека и помещение результата в стек
       3: ireturn    // возвращение целого числа из метода

В данном случае все операции занимают строго по одному байту.
Со списком операций можно познакомится по этой ссылке:
список инструкций Java байткода

Из набора таких операции и их параметров состоит любая программа скомпилированная javac. В свою очередь JVM, при запуске программы, начинает интерпретировать команды, превращая некоторые блоки программы в машинный код с помощью JIT технологии.

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

Понравилась статья? Посоветуйте другу

Количество коментариев: 1

  1. Максим

    Спасибо. Познавательно!

Добавить комментарий



[ Ctrl + Enter ]