Понимание байт-кода Java от 1+1=2

Java JVM

задний план

Не так давно вышло третье издание "Understanding Java Virtual Machine". Я быстро купил его и ознакомился с содержанием новой версии. Эта книга обновила многое из содержания новой версии виртуальной машины. , а также подвергли рефакторингу часть предыдущего контента. Стоит посмотреть. С целью обзора и консолидации я решил скомпилировать простой файл класса для анализа содержимого байт-кода Java, чтобы помочь понять и закрепить знания о байт-коде Java, и я надеюсь, что это будет полезно для вас, кто читает эту статью.

Описание: На этот раз используется среда OpenJdk12.

Скомпилировать код "1+1"

В первую очередь нам нужно написать простенькую небольшую программу, программу 1+1, и начинать обучение следует с самой простой 1+1, код такой:

package top.luozhou.test;

/**
 * @description:
 * @author: luozhou
 * @create: 2019-12-25 21:28
 **/
public class TestJava {
    public static void main(String[] args) {
        int a=1+1;
        System.out.println(a);
    }
}

После записи файла класса java сначала выполните командуjavac TestJava.javaСкомпилируйте файл класса, сгенерируйтеTestJava.class. Затем выполните команду декомпиляцииjavap -verbose TestJava, результат байт-кода отображается следующим образом:

  Compiled from "TestJava.java"
public class top.luozhou.test.TestJava
  minor version: 0
  major version: 56
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #17.#18        // java/io/PrintStream.println:(I)V
   #4 = Class              #19            // top/luozhou/test/TestJava
   #5 = Class              #20            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = Utf8               SourceFile
  #13 = Utf8               TestJava.java
  #14 = NameAndType        #6:#7          // "<init>":()V
  #15 = Class              #21            // java/lang/System
  #16 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;
  #17 = Class              #24            // java/io/PrintStream
  #18 = NameAndType        #25:#26        // println:(I)V
  #19 = Utf8               top/luozhou/test/TestJava
  #20 = Utf8               java/lang/Object
  #21 = Utf8               java/lang/System
  #22 = Utf8               out
  #23 = Utf8               Ljava/io/PrintStream;
  #24 = Utf8               java/io/PrintStream
  #25 = Utf8               println
  #26 = Utf8               (I)V
{
  public top.luozhou.test.TestJava();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_2
         1: istore_1
         2: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         5: iload_1
         6: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 10: 0
        line 11: 2
        line 12: 9
}

Разобрать байт-код

1. Основная информация

Приведенные выше результаты удаляют некоторую избыточную информацию, не влияющую на синтаксический анализ, а затем мы проанализируем результат байт-кода.

 minor version: 0 次版本号,为0表示未使用
 major version: 56 主版本号,56表示jdk12,表示只能运行在jdk12版本以及之后的虚拟机中
flags: ACC_PUBLIC, ACC_SUPER

ACC_PUBLIC: это флаг доступа, указывающий, является ли это общедоступным типом.

ACC_SUPER: Этот falg должен решитьinvokespecialПроблема с директивой, вызывающей суперметод. Думайте об этом как об исправлении ошибки для Java 1.0.2, только благодаря этому он может правильно находить методы суперкласса. Начиная с Java 1.0.2 компилятор всегда генерирует флаг доступа ACC_SUPER в байт-коде. Заинтересованные студенты могут нажать здесьздесьУзнать больше.

2. Постоянный пул

Далее мы проанализируем пул констант, вы также можете понять общий байт-код выше.

#1 = Methodref          #5.#14         // java/lang/Object."<init>":()V

Это ссылка на метод, здесь#5Представляет значение индекса, тогда мы можем обнаружить, что байт-код со значением индекса 5 выглядит следующим образом

#5 = Class              #20            // java/lang/Object

это говорит, что этоObjectкласс, то самое#14указывает на"<init>":()VУказывает, что это ссылка на метод инициализации.

#2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;

Вышеприведенный абзац указывает, что это ссылка на поле, которая также относится к#15а также#16, что на самом деле относится кjava/lang/Systemв классеPrintStreamобъект. Другие идеи анализа постоянного пула такие же. Из-за недостатка места я не буду объяснять их по одному. Ниже перечислены только несколько ключевых типов и информация.

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

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

#4 = Class              #19 
#19 = Utf8               top/luozhou/test/TestJava

это значит здесь#4Под этим индексом находится класс, а затем класс, на который указывает#19,#19ЯвляетсяUtf8таблица, конечное хранилищеtop/luozhou/test/TestJava, то после подключения мы можем знать, что#4Класс, на который ссылается местоположение,top/luozhou/test/TestJava.

3. Информация о методе строительства

Далее разберем байткод конструктора.Мы знаем, что при инициализации класса сначала выполняется его конструктор.Если вы не напишете конструктор, то система по умолчанию добавит вам конструктор без параметров.

public top.luozhou.test.TestJava();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0

descriptor: ()V: указывает, что это метод без возвращаемого значения.

flags: ACC_PUBLIC: это общедоступный метод.

stack=1, locals=1, args_size=1: указывает, что число в стеке равно 1, переменная в таблице локальных переменных равна 1 и параметр вызова также равен 1.

Почему здесь все 1? Разве это не конструктор по умолчанию? Откуда взялись параметры? На самом деле в языке Java есть негласное правило:В любом методе экземпляра вы можете передатьthisдля доступа к объекту, которому принадлежит этот метод. Реализация этого механизма передается в метод в качестве входного параметра через компилятор Java при компиляции, знакомый сpythonИзучающие языки обязательно узнают, что вpythonМетод, определенный в, всегда будет передаваться вselfПараметр, который также является ссылкой на этот экземпляр, переданный в метод, Java просто подталкивает этот механизм к этапу компиляции для завершения. Таким образом, 1 здесь относится кthisТолько этот параметр.

         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
    LineNumberTable:
        line 8: 0

После вышеприведенного анализа значение этого метода построения становится очень ясным.

aload_0: указывает, что первая переменная в таблице локальных переменных загружается в стек, т. е.this.

invokespecial: Непосредственный вызов метода инициализации.

return: вызов завершен, и метод завершается.

LineNumberTable:Это таблица количества строк, в которой записано соответствие между смещениями байт-кода и строками кода.line 8: 0Указывает, что 8-я строка в исходном коде соответствует смещению0Байт-код, так как это метод построения по умолчанию, не может быть здесь отражен напрямую.

Кроме того, здесь будет выполнятьсяObjectпостроен потому, чтоObjectЭто родительский класс для всех классов, и конструктор подкласса должен сначала создать конструктор родительского класса.

4. Информация об основном методе

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_2
         1: istore_1
         2: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         5: iload_1
         6: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 10: 0
        line 11: 2
        line 12: 9

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

stack=2, locals=2, args_size=1: Стек и таблица локальных переменных здесь равны 2, а параметр по-прежнему равен 1. Почему это? потому чтоmainВ методе объявлена ​​переменнаяa, поэтому таблицу локальных переменных нужно добавить на единицу, и стек тоже, поэтому их 2. так почемуargs_sizeИли 1? Вы не говорите, что по умолчаниюthisВходящий? Должно быть 2.Примечание. То, что я сказал ранее, относится к любому методу экземпляра, и этот основной метод является статическим методом. Доступ к статическому методу можно получить непосредственно через имя класса + метода, и ему не нужен объект экземпляра, поэтому нет необходимости передавать это здесь..

0: iconst_2:БудуintТип 2 помещается на вершину стека.

1: istore_1: положить вершину стекаintЗначение типа хранится во второй локальной переменной.

2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;:ПолучатьPrintStreamДобрый.

5: iload_1: поставь второйintЛокальная переменная типа помещается на вершину стека.

6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V:передачаprintlnметод. здесьprintlnМетод выполнит элемент наверху стека как свой собственный входной параметр и в конечном итоге выведет 2.

9: return: Вызвать метод конца, когда закончите.

здесьLineNumberTableИсходный код есть, можем проверить правильность моего предыдущего описания:

line 10: 0: Строка 10 означает0: iconst_2Байт-код, здесь мы обнаруживаем, что компилятор напрямую вычисляет за нас и заталкивает 2 на вершину стека.

line 11: 2: Строка 11 исходного кода соответствует2: getstaticСтатический класс для получения выводаPrintStream.

line 12: 9: 12 строк исходного кода соответствуютreturn, указывающий на конец метода.

Здесь я также рисую динамическую картинку, чтобы продемонстрироватьmainПроцесс выполнения метода, надеюсь, помог вам понять:

Суммировать

В этой статье я начал с компиляции исходного кода 1+1 и проанализировал сгенерированный байт-код Java, включая основную информацию о классе, пуле констант, процессе вызова метода и т. д. Благодаря этому анализу у нас есть хорошее понимание Java. У меня есть базовое понимание, и я также знаю, что компилятор Java отразит метод оптимизации через скомпилированный байт-код, такой как наш 1 + 1 = 2, байт байт-кода присваивает 2 переменной, а не добавляет ее. , таким образом оптимизируя наш код и повышая эффективность выполнения.

Ссылаться на

  1. не говорите .open JDK.java.net/browse/JDK-…