一、初识字节码
1.1 测试用例准备
所构造的Java测试类如下:
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;
}
}
1.2 编译和反编译
使用IDEA进行编译后产生class文件,使用 javap -v Test.class
分析。
λ javap -v Test.class
Classfile /E:/IDEA-Project/JVM/out/production/JVM/Test.class
Last modified 2021-7-3; size 757 bytes
MD5 checksum 96cabb2b8e3e791a984abfcd1676ab88
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#31 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#32 // Test.a:I
#3 = String #33 // Hello,World!
#4 = Fieldref #5.#34 // Test.s:Ljava/lang/String;
#5 = Class #35 // Test
#6 = Methodref #5.#31 // Test."<init>":()V
#7 = Methodref #36.#37 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#8 = Fieldref #5.#38 // Test.si:Ljava/lang/Integer;
#9 = Class #39 // java/lang/Object
#10 = Utf8 a
#11 = Utf8 I
#12 = Utf8 si
#13 = Utf8 Ljava/lang/Integer;
#14 = Utf8 s
#15 = Utf8 Ljava/lang/String;
#16 = Utf8 <init>
#17 = Utf8 ()V
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 LocalVariableTable
#21 = Utf8 this
#22 = Utf8 LTest;
#23 = Utf8 main
#24 = Utf8 ([Ljava/lang/String;)V
#25 = Utf8 args
#26 = Utf8 [Ljava/lang/String;
#27 = Utf8 test
#28 = Utf8 <clinit>
#29 = Utf8 SourceFile
#30 = Utf8 Test.java
#31 = NameAndType #16:#17 // "<init>":()V
#32 = NameAndType #10:#11 // a:I
#33 = Utf8 Hello,World!
#34 = NameAndType #14:#15 // s:Ljava/lang/String;
#35 = Utf8 Test
#36 = Class #40 // java/lang/Integer
#37 = NameAndType #41:#42 // valueOf:(I)Ljava/lang/Integer;
#38 = NameAndType #12:#13 // si:Ljava/lang/Integer;
#39 = Utf8 java/lang/Object
#40 = Utf8 java/lang/Integer
#41 = Utf8 valueOf
#42 = Utf8 (I)Ljava/lang/Integer;
{
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:
public Test();
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_3
6: putfield #2 // Field a:I
9: aload_0
10: ldc #3 // String Hello,World!
12: putfield #4 // Field s:Ljava/lang/String;
15: return
LineNumberTable:
line 1: 0
line 2: 4
line 4: 9
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 #5 // class Test
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: aload_1
9: bipush 8
11: putfield #2 // Field a:I
14: bipush 9
16: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: putstatic #8 // Field si:Ljava/lang/Integer;
22: return
LineNumberTable:
line 7: 0
line 8: 8
line 9: 14
line 10: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 args [Ljava/lang/String;
8 15 1 test LTest;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 6
2: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: putstatic #8 // Field si:Ljava/lang/Integer;
8: return
LineNumberTable:
line 3: 0
}
SourceFile: "Test.java"
在IDEA中安装插件 jclasslib
,并选择编译产生的class文件,查看。
1.3 查看字节码文件的二进制
使用HxD工具打开class文件。
二、魔数与版本
2.1 魔数
所有.class字节码文件的开始的4个字节代表魔数,并且其值一定为 0xCAFE BABE
(咖啡宝贝 =。=)
如果开始的4个字节不是CAFE BABE, 则 JVM 将会认为该文件不是 .class 字节码文件,会拒绝解析。
2.2 版本号
在魔数后面紧跟的4个字节分别为 次版本号
和 主版本号
;
可以看到这里的次版本号为0,主版本号为 00 34 即对应十进制的52。
JDK version | major主版本号 | minor 次版本号 |
---|---|---|
1.0 | 44 | 3 |
1.1 | 45 | 3 |
1.2 | 46 | 0 |
1.3 | 47 | 0 |
1.4 | 48 | 0 |
1.5 | 49 | 0 |
1.6 | 50 | 0 |
1.7 | 51 | 0 |
1.8 | 52 | 0 |
可以利用 jclasslib插件查看:
三、常量池
3.1 常量池常量数
后面2个字节代表常量池常量数,00 2B 对应十进制的 43;但是根据 javap 反编译后的文件( 上面的1.2 节 ),由于索引是从0开始的,但所有的字节码文件0位索引代表null,不显示出来,因此能看到的常量池数是 43-1 = 42个:
Constant pool:
#1 = Methodref #9.#31 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#32 // Test.a:I
#3 = String #33 // Hello,World!
#4 = Fieldref #5.#34 // Test.s:Ljava/lang/String;
#5 = Class #35 // Test
#6 = Methodref #5.#31 // Test."<init>":()V
#7 = Methodref #36.#37 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#8 = Fieldref #5.#38 // Test.si:Ljava/lang/Integer;
#9 = Class #39 // java/lang/Object
#10 = Utf8 a
#11 = Utf8 I
#12 = Utf8 si
#13 = Utf8 Ljava/lang/Integer;
#14 = Utf8 s
#15 = Utf8 Ljava/lang/String;
#16 = Utf8 <init>
#17 = Utf8 ()V
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 LocalVariableTable
#21 = Utf8 this
#22 = Utf8 LTest;
#23 = Utf8 main
#24 = Utf8 ([Ljava/lang/String;)V
#25 = Utf8 args
#26 = Utf8 [Ljava/lang/String;
#27 = Utf8 test
#28 = Utf8 <clinit>
#29 = Utf8 SourceFile
#30 = Utf8 Test.java
#31 = NameAndType #16:#17 // "<init>":()V
#32 = NameAndType #10:#11 // a:I
#33 = Utf8 Hello,World!
#34 = NameAndType #14:#15 // s:Ljava/lang/String;
#35 = Utf8 Test
#36 = Class #40 // java/lang/Integer
#37 = NameAndType #41:#42 // valueOf:(I)Ljava/lang/Integer;
#38 = NameAndType #12:#13 // si:Ljava/lang/Integer;
#39 = Utf8 java/lang/Object
#40 = Utf8 java/lang/Integer
#41 = Utf8 valueOf
#42 = Utf8 (I)Ljava/lang/Integer;
3.2 常量池的基本结构
-
java类所对应的常量池主要由常量池数量和常量池数组两部分组成, 常量池数组紧跟在常量池数量后面;
-
常量池数组中的不同的元素的类型,结构都是不同的,长度也是不同的;
-
但是每一种元素的第一个数据都是一个u1类型 ( 1字节 ),该字节是标志位;JVM 解析常量池时依靠一个标志位来获取该元素的具体类型;
3.2.1 JVM所定义的11种常量池元素类型
3.2.2 11种常量池元素类型的具体组成
常量池的每一种元素类型都是复合的数据结构类型,下面分别给出 JVM 所定义的常量池中每一种元素的具体结构。
3.2.3 第一个常量池元素
即对应
#1 = Methodref #9.#31 // java/lang/Object."<init>":()V
...
#9 = Class #39 // java/lang/Object
...
#16 = Utf8 <init>
#17 = Utf8 ()V
#31 = NameAndType #16:#17 // "<init>":()V
常量池的第一个元素的方法引用常量,引用的方法来自java/lang/Object这个类(#9 第9号常量池元素), 方法名和类型为(#31 第31号常量池元素), 可以看到 方法名和类型 指向#31号常量池元素,但是该元素又指向#16和#17, 同理可得#16项常量池元素为方法名,#17号常量池元素为类型; 即方法名为,类型为 ()V , 其中()为入参,括号内为空表示没有入参,V代表方法的返回类型为void。
即对应:
3.2.4 第二个常量池元素
第二个元素是 字段符号引用常量:
其指向的所属类名是 #5:Test类 , 指向的字段描述符(字段名字和类型)为 #32:a和I, 其中a为字段名,I为类型代表整数类型;
3.2.5 同理类推
当最后一个常量池元素结束时,class字节码文件对应到:
四、访问标识与继承信息
4.1 access_flags
在字节码文件中,常量数组之后紧跟着的是 access_flags
结构,该结构的类型是u2(2字节), access_flags
代表访问标志位,该标志用于标注类或接口层次的访问信息,例如 描述当前类是类还是接口,是否定义为public类型,是否定义为abstract类型等。
由于 Test.class
中的 access_flags=0x0021
,因此该类的访问表标识既包含了 ACC_PUBLIC
也包含了 ACC_SPUER
;
4.2 this_class
在字节码文件中, 紧跟 access_flags
访问标识之后的是 this_class
结构, 该结构是u2类型(2字节);
this_class
记录当前类的全限定名(包名+类名), 其值指向常量池中对应的索引值;
指向#5号常量池
#5 = Class #35 // Test
4.3 super_class
在字节码文件中, 紧跟 this_class
访问标识之后的是 super_class
结构, 该结构是u2类型(2字节);
this_class
记录当前类父类的全限定名(包名+类名), 其值指向常量池中对应的索引值;
指向#9号常量池
#9 = Class #39 // java/lang/Object
4.4 interface
4.4.1 interfaces_count
在字节码文件中, 紧跟 super_class
访问标识之后的是 interfaces_count
结构, 该结构是u2类型(2字节);
interfaces_count
记录当前类所实现的接口数量;
4.4.2 interfaces[interfaces_count]
在字节码文件中, 紧跟 interfaces_count
访问标识之后的是 interfaces[interfaces_count]
结构, 表示一组接口索引集合,是一组u2类型数据的集合(即每个接口使用2字节来索引常量池元素的标号),这些被实现的接口将按 implements
语句(如果该类为接口,则为 extend
语句) 后的接口顺序从左至右排列在接口的索引集合中;
由于Test.class的interfaces_count值为0,因此字节码文件中并没有interfaces信息
五、字段信息
5.1 fields_count
在字节码文件中, 紧跟 interfaces
访问标识之后的是 fields_count
结构, 该结构是u2类型(占2个字节)。 该值记录当前类中所定义的变量总数,包括类成员变量和类变量(即静态变量);
即对应:
5.2 fields_info_fields
在字节码文件中, 紧跟 fields_count
字段信息之后的是 fields_info_fields
结构, 该结构长度不确定,不用的变量类型所占的长度是不同的。
fileds
记录类中所定义的各个变量的详细信息,包括变量名,变量类型,访问标识和属性等;
1. fields的组成结构格式
2. **变量access_flags结构**
根据字节码文件,可以看到第一个域变量为 public, 变量名指向常量池#10,变量类型等描述指向常量池#11,并且属性数量为0, 因此并没有属性信息字段;
对应可以看:
需要注意的是: 类型的标识字符与类型的关系如下:
同理可得其他2个变量,可以分析得到:
对应到
六、方法信息
6.1 methods_count
在字节码文件中, 紧跟字段信息之后的是方法信息;首先的methods_count
结构,该结构是u2类型(占2字节)。
该结构描述类中的一共包含多少方法;
由上图分析可得,类中共有4个方法。但是类中明明就只有2个方法, 一个test方法,一个main方法。
需要注意的是:
1. 类中虽然没有写构造函数,但是编译器会自动为该类添加一个默认的构造函数
2. 编译期间,编译器会自动为该类增加 一个 `void <clinit>()` 方法, 其方法名就是 **<clinit>** ,返回值为 void;
3. <clinit>方法作用主要是执行类的初始化,源程序中的所有**static类型**的变量都会在这个方法中完成初始化,全部被 **static {}**
语句块所包围的程序都在这个方法中执行;
6.2 method_info methods
紧跟着methods_count
结构的是 methods结构
, 这是一个数组, 每一个方法的全部细节都包含在里面, 包括代码指令。
- methods结构的组成格式
6.3 第一个方法 void <init>
0x0001:access_flags, 说明该方法的访问属性为 public
0x0010:name_index, 该字段描述的是方法名, 其值指向常量池对应的元素编号, 对应#16
#16 = Utf8 <init>
**0x0011:descriptor_index,该字段描述的是方法的入参和出参信息, 其值指向常量池对应的元素编号,对应#17 **
#17 = Utf8 ()V ;; 说明入参为空,返回类型为void
0x0001:attributes_count, 该字段描述方法的属性数量,其值说明该方法有一个属性, 见下面的分析