Самый известный лозунг языка Java — «скомпилировать один раз и запускать везде», где «компилировать» означает, что компилятор компилирует исходный код Java в файлы байт-кода Java (то есть файлы .class, которые не различаются в этой статье). ), ""Выполнить" относится к виртуальной машине Java, выполняющей файл байт-кода. Кроссплатформенность Java выигрывает от реализации разных JVM на разных платформах.Пока предоставляется стандартизированный файл байт-кода, JVM любой платформы может выполняться, так что файл байт-кода можно запускать везде. Эта статья проанализирует структуру байт-кода на простом примере и углубит понимание механизма работы Java-программ.
1. Подготовьте файл .class
Первым шагом является подготовка файла байт-кода. Сначала напишите простую исходную программу JavaTestByteCode.java
:
package com.sinosun.test;
public class TestByteCode{
private int a = 1;
public String b = "2";
protected void method1(){}
public int method2(){
return this.a;
}
private String method3(){
return this.b;
}
}
использоватьjavac
Команда компилирует приведенный выше код для получения соответствующего файла TestByteCode.class, и на этом первый шаг завершен.
2. Ручной разбор файлов .class
После предыдущего шага был получен файл TestByteCode.class, который и является нужным нам байт-кодом. Давайте сначала посмотрим на содержимое файла. (Обратите внимание, что IDEA автоматически декомпилируется при открытии файла .class, здесь мы используем код из IDEAHexViewПлагин может просматривать файл .class, или вы можете использовать Sublime Text, чтобы напрямую открыть файл .class) Вы можете видеть, что файл байт-кода содержит много шестнадцатеричных байтов. .class файл.содержимое:
Чтобы понять файл класса, вы должны сначала узнать его структуру. Согласно спецификации байт-кода JVM, типичный файл класса состоит из десяти частей: MagicNumber, Version, Constant_Pool, Access_flag, This_class, Super_class, Interface, Fields, Method и Attributes. В байт-код включены два типа данных: беззнаковые числа и таблицы. Беззнаковые числа включают u1, u2, u4 и u8, которые представляют 1 байт, 2 байта, 4 байта и 8 байтов соответственно. Структура таблицы состоит из беззнаковых данных.
Согласно правилам, формат файла байт-кода фиксируется следующим образом:
Согласно приведенной выше таблице ясно видно, что байт-код принимает фиксированную файловую структуру и тип данных для достижения сегментации контента, структура очень компактна, в ней нет избыточной информации и нет разделителя.
3. Магический номер и номер версии
В соответствии со структурной таблицей содержимое первых четырех байтов файла .class является магическим числом файла .class. Магическое число является фиксированным значением:0xcafebabe
, что также является признаком того, что JVM распознает файл .class. Обычно мы различаем типы файлов по имени суффикса, но имя суффикса может быть изменено произвольно, поэтому виртуальная машина будет проверять эти четыре байта перед загрузкой файла класса, если нет.0xcafebabe
затем отказывается загружать файл.
Почему магическое число0xcafebabe
, пожалуйста, перейдите в DZone, чтобы посмотретьОбъяснение Джеймса Гослинга.
Номер версии следует за магическим числом и состоит из двух 2-байтовых полей, представляющих основной номер версии и дополнительный номер версии текущего файла .class соответственно. Соответствующая связь между номером версии и фактической версией JDK показана на рисунке. ниже. Номер версии скомпилированного файла .class связан с параметром -target, используемым во время компиляции.
Компилятор версии | -целевой параметр | шестнадцатеричное представление | десятичное представление |
---|---|---|---|
JDK 1.6.0_01 | без (по умолчанию - цель 1.6) | 00 00 00 32 | 50 |
JDK 1.6.0_01 | -target 1.5 | 00 00 00 31 | 49 |
JDK 1.6.0_01 | -target 1.4 -source 1.4 | 00 00 00 30 | 48 |
JDK 1.7.0 | без (по умолчанию - цель 1.6) | 00 00 00 32 | 50 |
JDK 1.7.0 | -target 1.7 | 00 00 00 33 | 51 |
JDK 1.7.0 | -target 1.4 -source 1.4 | 00 00 00 30 | 48 |
JDK 1.8.0 | Нет -целевой параметр | 00 00 00 34 | 52 |
В файле .class, полученном во втором разделе, значение, соответствующее магическому числу:0x0000 0034
, что указывает на то, что соответствующая версия JDK — 1.8.0, что соответствует версии JDK, используемой во время компиляции.
4. Постоянный пул
Константный пул — один из ключевых моментов разбора файлов .class.Во-первых, посмотрите на количество объектов в константном пуле. По второму разделу видно, чтоconstant_pool_count
значение0x001c
, преобразованный в десятичное число 28, в соответствии со спецификацией JVM,constant_pool_count
значение, равноеconstant_pool
Количество записей увеличивается на 1, поэтому в пуле констант 27 констант.
Согласно спецификации JVM общий формат констант в пуле констант следующий:
cp_info {
u1 tag;
u1 info[];
}
Существует 11 типов констант данных, их теги и содержимое показаны в следующей таблице:
Рассмотрим анализ констант на примере.На рисунке ниже красная линия является частью пула констант.
Сначала значение первого тега0x0a
, глядя на таблицу выше, мы видим, что константа соответствуетCONSTANT_Methodref_info
, то есть ссылка на метод. Два 2 байта после тега указывают на константу типа CONSTANT_Class_info и константу типа CONSTANT_NameAndType_info в пуле констант соответственно.Полные данные константы:0a 0006 0016
, шестой и постоянный индекс 22 констант в постоянном пуле из двух, согласно таблице, может знать значение:
0a 0006 0016 Methodref class#6 nameAndType#22
Поскольку 6-й и 22-й константы еще не были решены, вместо этого используются заполнители.
Таким же образом можно разобрать и другие константы, и полный пул констант, полученный в результате анализа, выглядит следующим образом:
серийный номер | шестнадцатеричное представление | значение | постоянное значение |
---|---|---|---|
1 | 0a 0006 0016 | Methodref #6 #22 | java/lang/Object."":()V |
2 | 09 0005 0017 | Fieldref #5 #23 | com/sinosun/test/TestByteCode.a:I |
3 | 08 0018 | String #24 | 2 |
4 | 09 0005 0019 | Fieldref #5 #25 | com/sinosun/test/TestByteCode.b:Ljava/lang/String; |
5 | 07 001a | Class #26 | com/sinosun/test/TestByteCode |
6 | 07 001b | Class #27 | java/lang/Object |
7 | 01 0001 61 | Кодировка UTF8 | a |
8 | 01 0001 49 | Кодировка UTF8 | I |
9 | 01 0001 62 | Кодировка UTF8 | b |
10 | 01 0012 4c6a6176612f6c616e672f537472696e673b | Кодировка UTF8 | Ljava/lang/String; |
11 | 01 0006 3c 69 6e 69 74 3e | Кодировка UTF8 | |
12 | 01 0003 28 29 56 | Кодировка UTF8 | ()V |
13 | 01 0004 43 6f 64 65 | Кодировка UTF8 | Code |
14 | 01 000f 4c696e654e756d6265725461626c65 | Кодировка UTF8 | LineNumberTable |
15 | 01 0007 6d 65 74 68 6f 64 31 | Кодировка UTF8 | method1 |
16 | 01 0007 6d 65 74 68 6f 64 32 | Кодировка UTF8 | method2 |
17 | 01 0003 28 29 49 | Кодировка UTF8 | ()I |
18 | 01 0007 6d 65 74 68 6f 64 33 | Кодировка UTF8 | method3 |
19 | 01 0014 28294c6a6176612f6c616e672f537472696e673b | Кодировка UTF8 | ()Ljava/lang/String; |
20 | 01 000a 53 6f 75 72 63 65 46 69 6c 65 | Кодировка UTF8 | SourceFile |
21 | 01 0011 5465737442797465436f64652e6a617661 | Кодировка UTF8 | TestByteCode.java |
22 | 0c 000b 000c | NameAndType #11 #12 | "":()V |
23 | 0c 0007 0008 | NameAndType #7 #8 | a:I |
24 | 01 0001 32 | Кодировка UTF8 | 2 |
25 | 0c 0009 000a | NameAndType #9 #10 | b:Ljava/lang/String; |
26 | 01 001d 636f6d2f73696e6f73756e2f746573 742f5465737442797465436f6465 | Кодировка UTF8 | com/sinosun/test/TestByteCode |
27 | 01 0010 6a6176612f6c616e672f4f626a656374 | Кодировка UTF8 | java/lang/Object |
В приведенной выше таблице показаны все константы, проанализированные из пула констант, и использование этих констант будет объяснено позже.
5. Знак доступа
access_flag
Определяет права доступа и атрибуты текущего файла .class. Как видно из таблицы ниже, информация, содержащаяся в этом флаге, включает в себя, является ли файл класса классом или интерфейсом, внешние права доступа, является ли онabstract
, если это класс, объявлен ли он какfinal
и Т. Д.
Flag Name | Value | Remarks |
---|---|---|
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | private |
ACC_PROTECTED | 0x0004 | protected |
ACC_STATIC | 0x0008 | static |
ACC_FINAL | 0x0010 | final |
ACC_SUPER | 0x0020 | Для совместимости с более ранними компиляторами новые компиляторы устанавливают этот флаг для использованияinvokespecialВыполните специальную обработку для методов подкласса при инструктировании. |
ACC_INTERFACE | 0x0200 | Интерфейс, также необходимо установить: ACC_ABSTRACT. Нельзя установить одновременно: ACC_FINAL, ACC_SUPER, ACC_ENUM |
ACC_ABSTRACT | 0x0400 | Абстрактный класс, не может быть создан. Не может быть установлено одновременно с ACC_FINAL. |
ACC_SYNTHETIC | 0x1000 | синтетический, созданный компилятором, не существует в исходном коде. |
ACC_ANNOTATION | 0x2000 | Тип аннотации (аннотации), необходимо установить одновременно: ACC_INTERFACE, ACC_ABSTRACT |
ACC_ENUM | 0x4000 | тип перечисления |
В файле Bytecode этой статьиaccess_flag
Значение флага0021
, значение не может быть напрямую запрошено в приведенной выше таблице, потому чтоaccess_flag
Значение представляет собой объединение серии битов флага,0x0021 = 0x0020+0x0001
, поэтому класс является общедоступным.
Флаги доступа также используются несколько раз в некоторых свойствах ниже.
6, тип индекса, родительский индекс, интерфейсный индекс
Индекс классаthis_class
Сохраняет индекс полного имени текущего класса в пуле констант, значение равно0x0005
, который указывает на пятую константу в пуле констант. Глядя на таблицу, мы видим, что содержимое:com/sinosun/test/TestByteCode
.
родительский индексsuper_class
Сохраняет индекс глобального квалифицированного имени родительского класса текущего класса в пуле констант, а значение равно0x0006
, указывает на шестую константу в пуле со значением:java/lang/Object
.
информация об интерфейсеinterfaces
Содержит список интерфейсов, реализованных текущим классом, включая количество интерфейсов и массив, содержащий индексы глобальных имен всех интерфейсов. Интерфейс не реализован в примере кода в этой статье, поэтому число равно 0.
7. Поле
Далее разбираем поляFields
раздел, первые два байтаfields_count
, значение0x0002
Число, указывающее, что поле равно 2. При этом структура каждого поля сfield_info
Выражать:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Согласно структуре для анализа двух полей, содержимое первого поля0002 0007 0008 0000
, флаг доступа0x0002
указывает, что полеprivate
тип, индекс имени указывает на 7-е значение в пуле константa
дескриптор типа указывает на 8-е значение в пуле константI
, количество связанных атрибутов равно 0, видно, что полеprivate I a
,вI
выражатьint
.
Аналогично, по0001 0009 000a 0000
Второе поле может быть проанализировано, и его значение равноpublic Ljava/lang/String; b
. один из нихLjava/lang/String;
выражатьString
.
Что касается соответствия между дескрипторами поля и исходным кодом, в следующей таблице простой иллюстрацию:
Дескриптор | исходный код |
---|---|
Ljava/lang/String; | String |
I | int |
[Ljava/lang/Object; | Object[] |
[Z | boolean[] |
[[Lcom/sinosun/generics/FileInfo; | com.sinosun.generics.FileInfo[][] |
8. Метод
Введите метод после конца поляmethods
Анализ , вы можете сначала увидеть, что количество методов0x0004
, всего четыре.
Это не правильно!TestByteCode.java
В Чжунмин всего три метода, почему?.class
Количество методов в файле стало 4?
Поскольку время компиляции автоматически генерирует<init>
метод в качестве конструктора класса по умолчанию.
Затем проанализируйте каждый метод, старые правила, сначала поймите определение формата метода, прежде чем анализировать:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
По формату сначала получаем первые 8 байт первого метода0001 000b 000c 0001
Согласно вышеуказанному формату и содержимому предыдущего постоянного пула и флагов доступа вы можете знать, что метод:public <init> ()V
, с прикрепленным атрибутом. Вы можете видеть, что имя метода<init>
. Для свойств, прикрепленных к методам, они имеют следующий формат:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
Продолжайте анализировать следующий контент000d
, запросите пул констант, чтобы узнать имя свойства:Code
.Code
собственностьmethod_info
Атрибут переменной длины в таблице атрибутов, который включает вспомогательную информацию об инструкциях и методах JVM, например методы инициализации экземпляра или методы инициализации класса или интерфейса. Если метод объявлен какnative
илиabstract
, то егоmethod_info
Таблица атрибутов в структуре не должна содержатьCode
Атрибуты. В противном случае его таблица атрибутов должна содержатьCode
Атрибуты.
Формат атрибута Code определяется следующим образом:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Проанализируйте последовательность байтов против структуры выше000d 00000030 0002 0001
, недвижимостьCode
атрибут, количество байтов, содержащихся в атрибуте, равно0x00000030
, то есть 48 байт, длина здесь не включает два поля индекс имени и длину.max_stack
Указывает максимальную глубину стека операндов, которая может быть достигнута при выполнении метода, которая равна 2;max_locals
Представляет количество локальных переменных, созданных во время выполнения метода, включая локальные переменные, используемые для передачи параметров методу во время выполнения.
Далее идет реальное логическое ядро метода — инструкции байт-кода, эти инструкции JVM и есть реальная реализация метода. прежде всегоcode_length
Указывает длину кода, значение здесь равно 16, что указывает на то, что следующие 16 байтов являются содержимым инструкции,2a b7 0001 2a 04 b5 0002 2a 12 03 b5 0004 b1
.
Для простоты понимания эти инструкции переведены в соответствующие мнемоники:
байт-код | мнемонический | Значение инструкции |
---|---|---|
0x2a | aload_0 | Поместите локальную переменную первого ссылочного типа на вершину стека |
0xb7 | invokespecial | Вызовите методы строительства суперкласса, методы инициализации экземпляра, частные методы |
0x04 | iconst_1 | поместить int 1 на вершину стека |
0xb5 | putfield | Присвоить значение полю экземпляра указанного класса |
0x12 | ldc | Поместить постоянное значение типа int, float или String из пула констант на вершину стека. |
0xb1 | return | вернуть void из текущего метода |
Из сравнительной таблицы видно, что значения этих команд таковы:
2a aload_0
b7 0001 invokespecial #1 //Method java/lang/Object."":()V
2a aload_0
04 iconst_1
b5 0002 putfield #2 //Field a:I
2a aload_0
12 03 ldc #3 //String 2
b5 0004 putfield #4 //Field b:Ljava/lang/String;
b1 return
Можно видеть, что в методе инициализации сама класс ссылается на это_class, переменная A и переменная B в классе нажимается на стек, и две переменные присваиваются значения, а затем кончается метод.
После анализа команды, метод таблицы исключений, настоящий метод не генерирует никаких исключений, поэтому длина таблицы0000
. Назад0001
Указывает, что за ним стоит атрибут. В соответствии с предыдущим форматом атрибута может быть известно, что индекс имени атрибута0x000e
, найдите постоянный пул, чтобы узнать, что свойствоLineNumberTable
Атрибуты.
НижеLineNumberTable
Структура имущества:
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
Объединенный структурный анализ0000000e 0003 0000 0003 0004 0004 0009 0005
Видно, что в этой таблице три элемента, первое число представляет позицию байта в коде инструкции, а второе число представляет номер строки в исходном коде.
Точно так же можно проанализировать последний метод.
Второй метод,0004 000f 000c 0001
Представляет имя метода и символ управления доступом в видеprotected method1 ()V
, с прикрепленным атрибутом.000d 00000019
, нет сомнений в том, что свойствоCode
, длина 25 байт.
0000 0001 00000001 b1
Видно, что глубина стека операндовmax_stack
0,max_locals
Значение 1 означает, что есть локальная переменная, и все методы будут иметь параметр, указывающий на класс, в котором они расположены по умолчанию. В теле метода есть только одна байтовая инструкция, котораяreturn
, так как метод является пустым методом.0000 0001
Указывает, что исключений нет и к нему прикреплен атрибут.000e 00000006 0001 0000 0007
собственностьLineNumberTable
, содержимое указывает, что первая байтовая инструкция соответствует строке 7 кода.
В последних двух методах используются три новые байтовые инструкции:
байт-код | мнемонический | Значение инструкции |
---|---|---|
0xb4 | getfield | Получить поле экземпляра указанного класса и поместить его на вершину стека |
0xac | ireturn | вернуть int из текущего метода |
0xb0 | areturn | Вернуть ссылку на объект из текущего метода |
Разобрать0001 0010 0011 0001 000d 0000 001d
Видно, что третий способpublic method2 ()I
,ТотCode
Содержимое атрибута0001 0001 00000005 2a b4 0002 ac
, получить переменнуюa
и вернуться. За ним по-прежнему следует сообщение об исключении иLineNumberTable
.
Четвертый способ здесь повторяться не будет.
0002 0012 0013 0001 000d 0000 001d private method3 ()Ljava/lang/String;
Code
0001 0001 00000005
2a b4 0004 b0 получить переменную b и вернуть
0000
LineNumberTable
0001 000e 00000006 0001 0000 000e //line 14 : 0
Таким образом, мы разбираем методы в классе в байт-коде. Байтовые инструкции являются ядром реализации метода. Байтовые инструкции соответствуют одной и той же операции в любой JVM, поэтому файлы байт-кода можно запускать на разных платформах. Однако детали реализации байтовых инструкций различаются на каждой платформе, что является настоящим «перекрестным» шагом Java-программ между разными платформами.
9. Свойства
Последняя часть - это свойства классаAttributes
, количество0x0001
,в соответствии сattribute_info
Для анализа недвижимости.
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
Первые два байта соответствуютname_index
,за0x0014
, то есть 20-я константа в пуле констант, просмотрите таблицу, чтобы получитьSourceFile
, что указывает на то, что свойствоSourceFile
Атрибуты. Этот атрибут является выбираемым длинным атрибутом в таблице атрибутов файла класса, и его структура выглядит следующим образом:
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}
Получить все содержимое этого свойства как0014 00000002 0015
По сравнению с постоянным столом показывает контент «SourceFile --testytecode.java», который обозначен.class
Файл исходного кода, соответствующий файлу.
10. Постскриптум
Это конец этой статьи.Если вы видите это, вы должны иметь базовое представление о структуре байт-кода.
Однако для того, что было сделано так много в прошлом, Java уже давно предоставляет инструмент командной строки.javap
все сделано, вступай.class
Папка, в которой находится файл, откройте инструмент командной строки и введите следующую команду:
javap -verbose XXX.class
Результат выглядит следующим образом:
PS E:\blog\Java字节码\资料> javap -verbose TestByteCode.class
Classfile /E:/blog/Java字节码/资料/TestByteCode.class
Last modified 2018-9-6; size 494 bytes
MD5 checksum 180292e6f6e8e9e48807195b235fa8ef
Compiled from "TestByteCode.java"
public class com.sinosun.test.TestByteCode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#22 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#23 // com/sinosun/test/TestByteCode.a:I
#3 = String #24 // 2
#4 = Fieldref #5.#25 // com/sinosun/test/TestByteCode.b:Ljava/lang/String;
#5 = Class #26 // com/sinosun/test/TestByteCode
#6 = Class #27 // java/lang/Object
#7 = Utf8 a
#8 = Utf8 I
#9 = Utf8 b
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 method1
#16 = Utf8 method2
#17 = Utf8 ()I
#18 = Utf8 method3
#19 = Utf8 ()Ljava/lang/String;
#20 = Utf8 SourceFile
#21 = Utf8 TestByteCode.java
#22 = NameAndType #11:#12 // "<init>":()V
#23 = NameAndType #7:#8 // a:I
#24 = Utf8 2
#25 = NameAndType #9:#10 // b:Ljava/lang/String;
#26 = Utf8 com/sinosun/test/TestByteCode
#27 = Utf8 java/lang/Object
{
public java.lang.String b;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC
public com.sinosun.test.TestByteCode();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: aload_0
10: ldc #3 // String 2
12: putfield #4 // Field b:Ljava/lang/String;
15: return
LineNumberTable:
line 3: 0
line 4: 4
line 5: 9
protected void method1();
descriptor: ()V
flags: ACC_PROTECTED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
public int method2();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 10: 0
}
SourceFile: "TestByteCode.java"
Это в основном результат, который мы получили из предыдущего анализа.
Конечно, мое первоначальное намерение поделиться этими процессами состоит не в том, чтобы надеяться, что я или мои читатели станут инструментом декомпиляции и с первого взгляда увидят истину байт-кода. Люди не могут делать эти вещи лучше, чем инструменты, но понимание этих вещей может помочь нам создавать лучшие инструменты, такие как CGlib, добавляя определенные операции перед загрузкой класса или напрямую динамически генерируя байт-код для достижения динамического прокси, который быстрее, чем динамический прокси JDK. с использованием java-рефлексии.
Я всегда считаю, что люди должны хорошо использовать инструменты, но они также должны быть любопытны и изучать детали, лежащие в основе этих инструментов. Что касается этой статьи, если вы можете рассказать всем больше о байт-коде, то цель достигнута. скобки лол