Java-байт-код
Компьютеры знают только 0 и 1. Это означает, что программа, написанная на любом языке, в конечном итоге должна быть скомпилирована в машинный код компилятором, прежде чем ее можно будет выполнить на компьютере. Поэтому программы, которые мы пишем, должны быть перекомпилированы, прежде чем их можно будет выполнять на разных платформах. Когда зародилась Java, был очень известный слоган:«Напиши один раз, беги куда угодно».
Write Once, Run Anywhere.
Для этой цели Sun и другие поставщики виртуальных машин выпустили множество виртуальных машин JVM, которые могут работать на разных платформах, и эти виртуальные машины имеют общую функцию, то есть они могут загружать и выполнять одну и ту же независимую от платформы виртуальную машину. Байткод). Таким образом, наш исходный код больше не нужно переводить в 0 и 1 в зависимости от разных платформ, а косвенно переводить в байт-код, а затем файл, хранящий байт-код, передается виртуальной машине JVM, работающей на разных платформах, для чтения и выполнения. , Для достижения цели написать один раз и запустить везде. Сегодня JVM больше не поддерживает только Java, и многие языки программирования на основе JVM были созданы на ее основе, например Groovy, Scala, Koltin и другие.
Семантика различных переменных, ключевых слов и операторов в исходном коде в конечном итоге компилируется в несколько команд байт-кода. Возможности семантического описания, предоставляемые командами байт-кода, значительно сильнее, чем у самой Java, поэтому существуют другие языки, основанные на JVM, которые могут предоставлять многие языковые функции, которые Java не поддерживает.
пример
Давайте шаг за шагом рассмотрим байт-код на простом примере.
//Main.java
public class Main {
private int m;
public int inc() {
return m + 1;
}
}
С помощью следующей команды вы можете создать файл по текущему путиMain.class
документ.
javac Main.java
Откройте сгенерированный файл класса в текстовом виде, содержание следующее:
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 4d61 696e 2e6a 6176 610c
0007 0008 0c00 0500 0601 0010 636f 6d2f
7268 7974 686d 372f 4d61 696e 0100 106a
6176 612f 6c61 6e67 2f4f 626a 6563 7400
2100 0300 0400 0000 0100 0200 0500 0600
0000 0200 0100 0700 0800 0100 0900 0000
1d00 0100 0100 0000 052a b700 01b1 0000
0001 000a 0000 0006 0001 0000 0003 0001
000b 000c 0001 0009 0000 001f 0002 0001
0000 0007 2ab4 0002 0460 ac00 0000 0100
0a00 0000 0600 0100 0000 0800 0100 0d00
0000 0200 0e
Для шестнадцатеричных кодов в файле, кроме началаcafe babe
, остальное можно примерно перевести в:Что это, черт подери, такое...
Не паникуйте, герои, давайте начнем со знакомой нам "малышки из кафе".
4 байта в начале файла называютсямагическое число, только файл класса, начинающийся с «cafe babe», может быть принят виртуальной машиной, и эти 4 байта являются идентификацией файла байт-кода.
Переместите глаза вправо, 0000 — это младший номер версии 0 версии компилятора jdk, 0034, преобразованный в десятичный, равен 52, что является основным номером версии, номер версии java начинается с 45, за исключением использования 1.0 и 1.1. 45.x, после каждого обновления основной версии номер версии увеличивается на единицу. То есть версия jdk, которая компилирует и генерирует файл класса, — 1.8.0.
пройти черезjava -version
После небольшой проверки команды можно получить результат.
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
Результат проверен.
Продолжая вниз постоянный бассейн. Но я не планирую продолжать анализировать этот шестнадцатеричный файл напрямую, это будет более утомительно, мы будем анализировать этот файл класса другим способом, более понятным для людей.
Декомпилировать файлы байт-кода
Используйте встроенный инструмент декомпиляции в javajavap
Файлы байт-кода могут быть декомпилированы.
пройти черезjavap -help
Разберитесь с основами использования javap
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
введите командуjavap -verbose -p Main.class
Проверьте вывод:
Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/Main.class
Last modified 2018-4-7; size 362 bytes
MD5 checksum 4aed8540b098992663b7ba08c65312de
Compiled from "Main.java"
public class com.rhythm7.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // com/rhythm7/Main.m:I
#3 = Class #20 // com/rhythm7/Main
#4 = Class #21 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/rhythm7/Main;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 Main.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 com/rhythm7/Main
#21 = Utf8 java/lang/Object
{
private int m;
descriptor: I
flags: ACC_PRIVATE
public com.rhythm7.Main();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/rhythm7/Main;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/rhythm7/Main;
}
SourceFile: "Main.java"
информация о файле байт-кода
Первые 7 строк информации включают в себя: текущее местоположение файла класса, время последней модификации, размер файла, значение MD5, из которого файл был скомпилирован, полное имя класса, дополнительный номер версии jdk и основной номер версии. Затем следует флаг доступа для класса:ACC_PUBLIC, ACC_SUPER, значение флага доступа следующее:
Название логотипа | Значение флага | значение |
---|---|---|
ACC_PUBLIC | 0x0001 | Является ли это общедоступным типом |
ACC_FINAL | 0x0010 | Объявлен ли он окончательным, только класс может установить |
ACC_SUPER | 0x0020 | Разрешить ли новую семантику инструкций по вызову специального байт-кода. |
ACC_INTERFACE | 0x0200 | флаг это интерфейс |
ACC_ABSTRACT | 0x0400 | Будь то абстрактный тип, интерфейс или абстрактный класс, Второстепенные флаги истинны, другие типы ложны |
ACC_SYNTHETIC | 0x1000 | Отмечает, что этот класс не был сгенерирован пользовательским кодом |
ACC_ANNOTATION | 0x2000 | флаг это аннотация |
ACC_ENUM | 0x4000 | флаг это перечисление |
постоянный пул
Constant pool
Означает постоянный пул.
Постоянный пул можно понимать как хранилище ресурсов в файле класса. Существует два основных типа констант: литеральные (Literal) и символьные ссылки (Symbolic References). Литералы похожи на константные понятия в java, такие как текстовые строки, конечные константы и т. д., тогда как символические ссылки относятся к концепции принципов компиляции, включая следующие три:
- Полное имя классов и интерфейсов
- Имя поля и дескриптор (Descriptor)
- имя и дескриптор метода
В отличие от C/C++, JVM выполняет динамическую компоновку при загрузке файла класса, что означает, что эти ссылки на символы полей и методов могут получить реальный адрес записи в памяти только после преобразования во время выполнения. Когда виртуальная машина работает, ей необходимо получить соответствующую ссылку на символ из пула констант, а затем проанализировать и преобразовать ее в определенный адрес памяти при создании или запуске класса.
Просмотрите содержимое байт-кода напрямую, декомпилировав файл:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#4 = Class #21 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#18 = NameAndType #7:#8 // "<init>":()V
#21 = Utf8 java/lang/Object
Первая константа — это определение метода, которое указывает на 4-ю и 18-ю константы. И так далее для 4-й и 18-й констант. Наконец, его можно вставить в содержимое комментария справа от первой константы:
java/lang/Object."<init>":()V
Этот абзац можно понимать как объявление конструктора экземпляра этого класса, поскольку класс Main не переопределяет конструктор, вызывается конструктор родительского класса. Здесь также указано, что прямым родительским классом основного класса является Object. Возвращаемое значение по умолчанию этого метода — V, которое является недействительным и не возвращает значения.
Аналогично можно проанализировать вторую константу:
#2 = Fieldref #3.#19 // com/rhythm7/Main.m:I
#3 = Class #20 // com/rhythm7/Main
#5 = Utf8 m
#6 = Utf8 I
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 com/rhythm7/Main
Здесь объявлено поле m, тип I и I тип int. Типы байт-кодов соответствуют следующим:
идентификационный символ | значение |
---|---|
B | байт основного типа |
C | символ основного типа |
D | двухместный базовый тип |
F | поплавок базового типа |
I | основной тип int |
J | базовый тип длинный |
S | базовый тип короткий |
Z | логическое значение базового типа |
V | пустота особого типа |
L | Тип объекта, заканчивающийся точкой с запятой, например Ljava/lang/Object; |
Для типов массивов каждый бит описывается предшествующим символом «[», например, определение размерного массива типа java.lang.String[][], он будет записан как «[[Ljava/lang/String;» |
Коллекция таблиц методов
После константного пула идет описание методов внутри класса, которое представлено в виде набора таблиц в байт-коде.Пока, вне зависимости от содержимого шестнадцатеричного файла файла байт-кода, мы непосредственно смотрим в декомпилированном контенте.
private int m;
descriptor: I
flags: ACC_PRIVATE
Здесь объявляется приватная переменная m, тип — int, возвращаемое значение — int.
public com.rhythm7.Main();
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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/rhythm7/Main;
Вот конструктор: Main(), возвращаемое значение void, открытый метод. Основные свойства кода:
- stack
Максимальный стек операндов, среда выполнения JVM будет выделять глубину стека операций в кадре стека (Frame) в соответствии с этим значением, здесь 1
- locals:
Объем памяти, необходимый для локальных переменных, в единицах Slot — это наименьшая единица, используемая виртуальной машиной при выделении памяти для локальных переменных, и ее размер составляет 4 байта. Параметры метода (включая скрытый параметр this в методе экземпляра), параметры, которые отображают обработчик исключений (исключение, определенное блоком catch в try catch), и локальные переменные, определенные в теле метода, должны храниться в таблица локальных переменных. Стоит отметить, что размер локальных переменных не обязательно равен сумме слотов, занятых всеми локальными переменными, потому что слоты в локальных переменных можно использовать повторно.
- args_size:
Количество параметров метода здесь равно 1, потому что каждый метод экземпляра будет иметь скрытый параметр.
- attribute_info
Содержимое тела метода, 0, 1 и 4 - это "номера строк" байт-кода Смысл этого кода в том, чтобы поместить первую локальную переменную ссылочного типа на вершину стека, а затем выполнить метод экземпляра того типа, который хранится в пуле констант. Первая переменная , которая является «java/lang/Object.»:()V» в комментарии, а затем выполните оператор возврата, чтобы завершить метод.
- LineNumberTable
Функция этого свойства — описать соответствие между номером исходной строки и номером строки байт-кода (смещение байт-кода). Вы можете использовать параметр -g:none или -g:lines, чтобы отменить или запросить генерацию этой информации.Если вы решите не генерировать LineNumberTable, когда программа работает ненормально, вы не сможете получить номер строки исходный код, где произошло исключение, и вы не сможете проследить номер строки исходного кода для отладки программы.
- LocalVariableTable
Функция этого атрибута заключается в описании отношения между локальными переменными в стеке кадров и переменными, определенными в исходном коде. Вы можете использовать -g:none или -g:vars, чтобы отменить или сгенерировать эту информацию. Если эта информация не сгенерирована, то, когда другие обратятся к этому методу, они не смогут получить имена параметров. Вместо этого arg0, arg1 используются битовый символ. start указывает, в какой строке видна локальная переменная, length указывает количество видимых строк, Slot указывает положение стека фреймов, Name — имя переменной, а затем сигнатура типа.
Аналогичным образом можно проанализировать другой метод «inc()» в классе Main: Содержимое тела метода: поместите это в стек, получите поле №2 и поместите его на вершину стека, поместите 1 типа int в стек, добавьте два значения на вершину стека, и вернуть значение типа int.
SourceFile
имя исходного файла
настоящий бой
Проанализируйте попытку-поймать-наконец-то
На простейшем примере выше можно примерно понять, как выглядит исходный код после его компиляции в байт-код. В следующем примере используются знания, полученные для анализа проблемы Java: какое значение вернет метод, когда возникнет исключение, и когда исключение не возникнет? Сначала подумай, а потом посмотрим на результаты.
public class TestCode {
public int foo() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
}
Каково возвращаемое значение foo(), когда исключение не возникает и когда возникает исключение. использовать старые методы
javac TestCode.java
javap -verbose TestCode.class
Просмотрите содержимое метода foo байт-кода:
public int foo();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1 //int型1入栈 ->栈顶=1
1: istore_1 //将栈顶的int型数值存入第二个局部变量 ->局部2=1
2: iload_1 //将第二个int型局部变量推送至栈顶 ->栈顶=1
3: istore_2 //!!将栈顶int型数值存入第三个局部变量 ->局部3=1
4: iconst_3 //int型3入栈 ->栈顶=3
5: istore_1 //将栈顶的int型数值存入第二个局部变量 ->局部2=3
6: iload_2 //!!将第三个int型局部变量推送至栈顶 ->栈顶=1
7: ireturn //从当前方法返回栈顶int数值 ->1
8: astore_2 // ->局部3=Exception
9: iconst_2 // ->栈顶=2
10: istore_1 // ->局部2=2
11: iload_1 //->栈顶=2
12: istore_3 //!! ->局部4=2
13: iconst_3 // ->栈顶=3
14: istore_1 // ->局部1=3
15: iload_3 //!! ->栈顶=2
16: ireturn // -> 2
17: astore 4 //将栈顶引用型数值存入第五个局部变量=any
19: iconst_3 //将int型数值3入栈 -> 栈顶3
20: istore_1 //将栈顶第一个int数值存入第二个局部变量 -> 局部2=3
21: aload 4 //将局部第五个局部变量(引用型)推送至栈顶
23: athrow //将栈顶的异常抛出
Exception table:
from to target type
0 4 8 Class java/lang/Exception //0到4行对应的异常,对应#8中储存的异常
0 4 17 any //Exeption之外的其他异常
8 13 17 any
17 19 17 any
Та же операция выполняется в 4, 5 и 13, 14 байт-кода, которая заключается в том, чтобы поместить тип int 3 на вершину стека операндов и сохранить его во второй локальной переменной. Это именно то, что наш исходный код содержит в блоке finally. Другими словами, когда JVM обрабатывает исключения, она многократно выполняет оператор finally в каждой возможной ветви, а затем выполняет оператор return в конце. Однако стоит отметить, что операция присваивания x в блоке операторов finally не будет действовать в соответствии с последовательностью операций входа и выхода переменной и операций присваивания. Итак, окончательный результат операции:
- Когда исключения не возникает: вернуть 1
- В случае исключения: вернуть 2
- Исключение, отличное от Exception и его подклассов, вызывает исключение и не возвращает значение
Приведенный выше пример взят из «Глубокого понимания расширенных функций и лучших практик виртуальной машины Java JVM». О таблице инструкций байт-кода виртуальной машины вы также можете узнать в «Углубленном понимании расширенных функций и передовых методов виртуальной машины Java JVM — Приложение B».
Реализация расширения функции kotlin
Kotlin предоставляет языковую функцию расширенных функций, с помощью которой мы можем добавлять пользовательские методы к любому объекту. В следующем примере к объекту добавляется метод «sayHello».
//SayHello.kt
package com.rhythm7
fun Any.sayHello() {
println("Hello")
}
После компиляции используйте javap для просмотра байт-кода, создавшего файл SayHelloKt.class.
Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/SayHelloKt.class
Last modified 2018-4-8; size 958 bytes
MD5 checksum 780a04b75a91be7605cac4655b499f19
Compiled from "SayHello.kt"
public final class com.rhythm7.SayHelloKt
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool:
//省略常量池部分字节码
{
public static final void sayHello(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: ldc #9 // String $receiver
3: invokestatic #15 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: ldc #17 // String Hello
8: astore_1
9: getstatic #23 // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_1
13: invokevirtual #28 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
16: return
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 $receiver Ljava/lang/Object;
LineNumberTable:
line 4: 6
line 5: 16
RuntimeInvisibleParameterAnnotations:
0:
0: #7()
}
SourceFile: "SayHello.kt"
Наблюдая за заголовком, koltin сгенерировал класс для файла SayHello, имя класса — «com.rhythm7.SayHelloKt».
Поскольку мы не ожидали, что SayHello будет инстанцируемым объектным классом, когда мы впервые написали SayHello.kt, SayHelloKt не может быть создан, и SayHelloKt не имеет никакого конструктора.
Есть только один способ снова наблюдать: обнаружитьAny.sayHello()
Конкретная реализация представлена в виде статического неизменного метода:
public static final void sayHello(java.lang.Object);
поэтому, когда мы используем в другом местеAny.sayHello()
когда на самом деле эквивалентно вызову javaSayHelloKt.sayHello(Object)
метод.
Кстати, когда расширенный метод Any, это значит, что Any не null, в это время компилятор проверит ненулевой параметр в начале тела метода, то есть вызоветkotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(Object value, String paramName)
метод, чтобы проверить, является ли переданный объект типа Any пустым. Если наша расширенная функцияAny?.sayHello()
, то этот байт-код не появится в скомпилированном файле.