Java字节码

摘要

  • Java字节码的二进制格式
  • Java字节码的魔数与版本
  • Java字节码的常量池
  • Java字节码的类继承
  • Java字节码的字段存储
  • Java字节码的方法格式

要想深刻理解JVM执行引擎的机制,就必须对JVM内部的数据结构有深入了解,而要了解JVM内部的数据结构,就必须要了解从Java源程序过渡到JVM内部数据结构的中间桥梁——Java字节码。

字节码格式

首先准备一个测试用例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public int a = 3;
static Integer si = 6;
String s = "Hello world!";
public static void main(String[] args) {
Test test = new Test();
test.a = 8;
si = 9;
}
private void test() {
this.a = a;
}
}

这个测试类虽然简单,然而五脏俱全,包含类变量、成员变量、字符串和类成员方法,同时包含一个入口函数main(),并且在main()函数中实例化了Test类。

接着编译Test.java类,得到Test.class文件。进入Test.java文件所在目录,输入如下命令:

1
javap -verbose Test

执行javap命令后将输出如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // Test
#2 = Utf8 Test
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 si
#8 = Utf8 Ljava/lang/Integer;
#9 = Utf8 s
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 <clinit>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Methodref #15.#17 // java/lang/Integer.valueOf:(I)Ljava/
lang/Integer;
#15 = Class #16 // java/lang/Integer
#16 = Utf8 java/lang/Integer
#17 = NameAndType #18:#19 // valueOf:(I)Ljava/lang/Integer;
#18 = Utf8 valueOf
#19 = Utf8 (I)Ljava/lang/Integer;
#20 = Fieldref #1.#21 // Test.si:Ljava/lang/Integer;
#21 = NameAndType #7:#8 // si:Ljava/lang/Integer;
#22 = Utf8 LineNumberTable
#23 = Utf8 LocalVariableTable
#24 = Utf8 <init>
#25 = Methodref #3.#26 // java/lang/Object."<init>":()V
#26 = NameAndType #24:#12 // "<init>":()V
#27 = Fieldref #1.#28 // Test.a:I
#28 = NameAndType #5:#6 // a:I
#29 = String #30 // Hello world!
#30 = Utf8 Hello world!
#31 = Fieldref #1.#32 // Test.s:Ljava/lang/String;
#32 = NameAndType #9:#10 // s:Ljava/lang/String;
#33 = Utf8 this
#34 = Utf8 LTest;
#35 = Utf8 main
#36 = Utf8 ([Ljava/lang/String;)V
#37 = Methodref #1.#26 // Test."<init>":()V
#38 = Utf8 args
#39 = Utf8 [Ljava/lang/String;
#40 = Utf8 test
#41 = Utf8 SourceFile
#42 = Utf8 Test.java
{
public int a;
descriptor: I
flags: ACC_PUBLIC
static java.lang.Integer si;
descriptor: Ljava/lang/Integer;
flags: ACC_STATIC
java.lang.String s;
descriptor: Ljava/lang/String;
flags:
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 6
2: invokestatic #14 // Method java/lang/Integer.valueO
f:(I)Ljava/lang/Integer;
5: putstatic #20 // Field si:Ljava/lang/Integer;
8: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #25 // Method java/lang/Object."<init>
":()V
4: aload_0
5: iconst_3
6: putfield #27 // Field a:I
9: aload_0
10: ldc #29 // String Hello world!
12: putfield #31 // Field s:Ljava/lang/String;
15: return
LineNumberTable:
line 2: 0
line 4: 4
line 6: 9
line 2: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this LTest;
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: new #1 // class Test
3: dup
4: invokespecial #37 // Method "<init>":()V
7: astore_1
8: aload_1
9: bipush 8
11: putfield #27 // Field a:I
14: bipush 9
16: invokestatic #14 // Method java/lang/Integer.valueO
f:(I)Ljava/lang/Integer;
19: putstatic #20 // Field si:Ljava/lang/Integer;
22: return
LineNumberTable:
line 9: 0
line 10: 8
line 11: 14
line 12: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 args [Ljava/lang/String;
8 15 1 test LTest;
}
SourceFile: "Test.java"

使用javap -verbose命令分析一个字节码文件时,将会分析字节码文件的魔数、版本号、常量池、类信息、类的构造函数、类中所包含的方法信息以及类(成员)变量信息。

下面使用十六进制工具打开Test.class文件,查看字节码二进制表示的文件内容。

魔数与版本

基于上文测试用例中所使用的Test.class字节码文件,开始分析字节码文件的组成格式(一共10个组成部分)。

所有.class字节码文件的开始4个字节都是魔数,并且其值一定是0xCAFEBABE。这里的0xCAFEBABE是指十六进制数值,并不是字符串“CAFEBABE”。如果开始4字节不是0xCAFEBABE,则JVM将会认为该文件不是.class字节码文件,并拒绝解析。

根据字节码文件规范,魔数之后的4个字节为版本信息,前两个字节表示major version,即主版本号;后两个字节表示minor version,即次版本号。

常量池

常量池是.class字节码文件中非常重要和核心的内容,一个Java类中绝大部分的信息都由常量池描述,尤其是Java类中定义的变量和方法,都由常量池保存。在JVM的内存模型中,堆区的常量池就是用于保存每一个Java类所对应的常量池的信息的,一个Java应用程序中所包含的所有Java类的常量池,组成了JVM堆区中大的常量池。

  • 常量池的基本结构

    Java类所对应的常量池主要由常量池数量和常量池数组两部分组成,常量池数量紧跟在次版本号的后面,占2字节。常量池数组则紧跟在常量池数量之后。

    常量池数组,就是一个类似数组的结构。这个数组固化在字节码文件中,由多个元素组成。与一般数组概念不同的是,常量池数组中不同的元素的类型、结构都是不同的,长度也是不同的,但是每一种元素的第一个数据都是一个u1类型,该字节是标志位,占1个字节。JVM解析常量池时,根据这个u1类型来获取该元素的具体类型。常量池的结构组成如下图所示。

    Java字节码中常量池的组成结构

    使用结构化的方式来描述常量池数组的编排,可以如下描述:

    tag1 元素内容1 tag2 元素内容2 tag3 元素内容3 … tagn 元素内容n

    常量池元素中的不同元素结构与类型都是不同的,正因如此,JVM只能定义有限的元素类型,并针对有限的类型进行专门解析。JVM一共定义了11中常量。

    | 编号 | 常量池元素名称 | tag位标志 | 含义 |
    | —- | ——————————– | —— | —————– |
    | 1 | CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
    | 2 | CONSTANT_Integer_info | 3 | 整形字面量 |
    | 3 | CONSTANT_Float_info | 4 | 浮点型字面量 |
    | 4 | CONSTANT_Long_info | 5 | 长整型字面量 |
    | 5 | CONSTANT_Double_info | 6 | 双精度字面量 |
    | 6 | CONSTANT_Class_info | 7 | 类或接口的符号引用 |
    | 7 | CONSTANT_String_info | 8 | 字符串类型的字面量 |
    | 8 | CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
    | 9 | CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
    | 10 | CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
    | 11 | CONSTANT_NameAndType_info | 12 | 字段和方法的名称以及类型的符号引用 |

    可以看到,类的方法信息、接口和集成信息、属性信息都是定义在NameAndType_info中的。

  • 常量池元素的复合结构

    常量池数组的每一种元素的内容都是复合数据结构的,下面分别给出JVM所定义的常量池中每一种元素的具体结构。

    常量池元素结构

  • 常量池的结束位置

    JVM在解析常量池时,class文件给出了常量池的总数,凡是碰到有bytes数组的常量池元素,class文件在常量池的每一个元素之前都会专门划分出2字节用于描述该常量池元素内容所占的字节长度,这样一来,常量池最终每一个元素的长度是确定的,而常量池的总数也是确定的,JVM据此便可以从class文件中准确地计算出常量池结构体的末端位置。

访问标识与继承信息

  • access_flags

    在字节码文件中,常量池数组之后紧跟着的是access_flags结构,该结构类型是u2,占2字节。access_flags代表访问标志位,该标志用于标注类或接口层次的访问信息,如当前Class是类还是接口,是否定义为public类型,是否定义为abstract类型等。access_flags的可选项值如下表所示。

    | 标志名称 | 标志值 | 含义 |
    | ————– | —— | —————————————- |
    | ACC_PUBLIC | 0x0001 | 是否为public类型 |
    | ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
    | ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令,JDK1.2以后变异出来的类这个标志为真 |
    | ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
    | ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口和抽象类,次标志为真,其他类为假 |
    | ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码生成 |
    | ACC_ANNOTATION | 0x2000 | 表示这是一个注解 |
    | ACC_ENUM | 0x4000 | 表示这是一个枚举 |

    由于Test.class中的access_flags = 0x21,因此该类的访问标识既包含ACC_PUBLIC(0x0001),也包含ACC_SUPER(0x0020)。其中,自JDK1.2以后,类被编译出来的invokespecial字节码指令是否允许使用的选项都是真,因此access_flags的值都会带有ACC_SUPER标识位。

  • this_class

    在字节码文件中,紧跟着access_flags访问标识之后的是this_class结构,该结构类型是u2,占2字节。this_class记录当前类的全限定名(包名 + 类名),其值指向常量池中对应的索引值。

  • super_class

    在字节码文件中,紧跟着this_class访问标识之后的是super_class结构,该结构类型是u2,占2字节。super_class记录当前类的父类全限定名,其值指向常量池中对应的索引值。

  • interface

    ①interfaces_count

    在字节码文件中,紧跟着super_class访问标识之后的是interfaces_count结构,该结构类型是u2,占2字节。interfaces_count结构记录当前类所实现的接口数量。

    ②interfaces[interfaces_count]

    interfaces标识接口索引集合,是一组u2类型数据的集合,该结构描述当前类实现了哪些接口,这些被实现的接口将按implement语句(如果该类本身为接口,则为extends语句)后的接口顺序从左到右排列在接口的索引集合中。

字段信息

  • fields_count

    在字节码文件中,接口区之后紧跟着是fields_count结构,该结构类型是u2,占2字节。该值记录当前类中所定义的变量总数量,包括类成员变量和类变量(即静态变量)。

  • fields_info fields[fields_count]

    在字节码文件中,紧跟着fields_count之后的是fields结构,该结构长度不确定,不同的变量类型所占长度是不同的。fields记录类中所定义的各个变量的详细信息,包括变量名、变量类型、访问标识、属性等。

    fields结构组成格式

    | 类型 | 名称 | 数量 |
    | ————— | —————- | —————- |
    | u2 | access_flags | 1 |
    | u2 | name_index | 1 |
    | u2 | descriptor_index | 1 |
    | u2 | attributes_count | 1 |
    | attriibute_info | attributes | attributes_count |

    access_flags,标识变量的访问标志,该值是可选的,有JVM规范规定。

    access_flags的可选项值如下表所示。

    | 标志名称 | 标志值 | 含义 |
    | ————- | —— | ————– |
    | ACC_PUBLIC | 0x0001 | 字段是否为public |
    | ACC_PRIVATE | 0x0002 | 字段是否为private |
    | ACC_PROTECTED | 0x0004 | 字段是否为protected |
    | ACC_STATIC | 0x0008 | 字段是否为static |
    | ACC_FINAL | 0x0010 | 字段是否为final |
    | ACC_VOLATILE | 0x0040 | 字段是否为volatile |
    | ACC_TRANSIENT | 0x0080 | 字段是否为transient |
    | ACC_SYNTHE | 0x1000 | 字段是否为为编译器自动生成 |
    | ACC_ENUM | 0x4000 | 字段是否为enum |

    name_index,标识变量的简单名称引用,占2字节,其值指向常量池的索引。

    descriptor_index,标识变量的类型信息引用,占2字节,其值指向常量池的索引。

    fields结构体实际上是一个数组,数组中的每一个元素都包含访问标识、名称索引、描述信息索引、属性数量和属性信息。

方法信息

  • methods_count

    在字节码文件中,紧跟着变量描述结构fields后面的是methods_count结构,该结构类型是u2,占2字节。该结构描述类中一共包含多少个方法。

    在编译期间,编译器会自动为一个类增加void < clinit >()这样一个方法,其方法名就是”< clinit >”,返回值为void。该方法的作用主要是执行类的初始化,源代码中所有的static类型的变量都会在这个方法中完成初始化,全部被static{}所包围的程序都会在这个方法中执行。同时,在源代码中,如果没有为类定义构造函数,编译器会自动为该类添加一个默认的构造函数。

  • methods_info methods[methods_count]

    紧跟在methods_count后面的是methods结构,这是一个数组,每一个方法的全部细节都包含在里面,包含代码指令。

    methods结构组成格式

    | 类型 | 名称 | 数量 |
    | ————– | —————- | —————- |
    | u2 | access_flags | 1 |
    | u2 | name_index | 1 |
    | u2 | descriptor_index | 1 |
    | u2 | attributes_count | 1 |
    | attribute_info | attributes | attributes_count |

    access_flags的可选项值如下表所示。

    | 标志名称 | 标志值 | 含义 |
    | —————- | —— | —————– |
    | ACC_PUBLIC | 0x0001 | 字段是否为public |
    | ACC_PRIVATE | 0x0002 | 字段是否为private |
    | ACC_PROTECTED | 0x0004 | 字段是否为protected |
    | ACC_STATIC | 0x0008 | 字段是否为static |
    | ACC_FINAL | 0x0010 | 字段是否为final |
    | ACC_SYNCHRONIZED | 0x0020 | 字段是否为synchronized |
    | ACC_BRIGED | 0x0040 | 方法是否是由编译器产生的桥接方法 |
    | ACC_VARARGS | 0x0080 | 方法是否接收补丁参数 |
    | ACC_NATIVE | 0x0100 | 字段是否为native |
    | ACC_ABSTRACT | 0x0400 | 字段是否为abstract |
    | ACC_STRICTFP | 0x0800 | 字段是否为strictfp |
    | ACC_SYNTHETIC | 0x1000 | 字段是否为编译器自动产生 |

    在class文件中,属性表、方法表中都可以包含自己的属性表集合,用于描述某些场景的专有信息,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性,如下表所示。

    | 属性名称 | 使用位置 | 含义 |
    | —————— | ———– | ——————– |
    | Code | 方法表 | Java代码编译成的字节码指令 |
    | ConstantValue | 字段表 | final关键字定义的常量表 |
    | Deprecated | 类文件、字段表、方法表 | 被声明为deprecated的方法和字段 |
    | Exception | 方法表 | 方法抛出的异常 |
    | InnerClasses | 类文件 | 内部类列表 |
    | LineNumberTale | Code属性 | Java源码的行号与字节码指令的对应关系 |
    | LocalVariableTable | Code属性 | 方法的局部变量描述 |
    | SourceFile | 类文件 | 源文件名称 |
    | Synthetic | 类文件、字段表、方法表 | 标识方法或字段是由编译器自动生成的 |

    这9个属性中的每一种属性又都是一个复合结构,均有各自的表结构。这9中表结构有一个共同的特点,即均由一个u2类型的属性名称开始,可以通过这个属性名称来判断属性的类型。该u2类型的属性名称指向常量池中对应的元素。