对于jvm来说,输入的是class字节码文件。而不是java源码文件。java的执行过程是通过编译器将java源码文件编译为class文件。在通过jvm中的编译器将源码编译为机器码进行执行

1 字节码文件格式

《Java 虚拟机规范》规定了 Java 虚拟机结构、Class 类文件结构、字节码指令等内容。字节码文件结构是一组以 8 位字节为基础的二进制流,各数据项目严格按照顺序紧凑地排列在 Class 文件之中,中间没有添加任何分隔符。在字节码结构中,有两种最基本的数据类型来表示字节码文件格式,分别是:无符号数和表。

无符号数属于最基本的数据类型它以 u1、u2、u4、u8 六七分别代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成的字符串值。例如下表中第一行中的 u4 表示 Class 文件前 4 个字节表示该文件的魔数,第二行的 u2 表示该 Class 文件第 5-6 个字节表示该 JDK 的次版本号。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型。所有表都习惯性地以_info结尾。表用于描述有层次关系的复合结构的数据,例如下表第 5 行表示其实一个类型为 cp_info 的表(常量池),这里面存储了该类的所有常量




java字节码文件的扩展名是( ) java字节码文件类型_java


2 具体探索

参考上面的表格可以将一个完整的class文件划分为7个模块:

  • 魔数和class的版本
  • 常量池
  • 访问标识
  • 类索引,父类索引,接口索引
  • 字段集合
  • 方法集合
  • 属性集合

首先编写一段代码,输出简单的hello world字符串。代码内容如下

public class Test{ public static void main(String args[]){ String a = "Hello Wolrd."; System.out.println(a); }}

使用java自带的编译命令来对这段代码进行编译。编译命令: javac Test.java

cafe babe 0000 0034 001d 0a00 0600 0f080010 0900 1100 120a 0013 0014 0700 15070016 0100 063c 696e 6974 3e01 0003 28295601 0004 436f 6465 0100 0f4c 696e 654e756d 6265 7254 6162 6c65 0100 046d 61696e01 0016 285b 4c6a 6176 612f 6c61 6e672f53 7472 696e 673b 2956 0100 0a53 6f757263 6546 696c 6501 0009 5465 7374 2e6a6176 610c 0007 0008 0100 0c48 656c 6c6f2057 6f6c 7264 2e07 0017 0c00 1800 1907001a 0c00 1b00 1c01 0004 5465 7374 0100106a 6176 612f 6c61 6e67 2f4f 626a 65637401 0010 6a61 7661 2f6c 616e 672f 53797374 656d 0100 036f 7574 0100 154c 6a617661 2f69 6f2f 5072 696e 7453 7472 65616d3b 0100 136a 6176 612f 696f 2f50 72696e74 5374 7265 616d 0100 0770 7269 6e746c6e 0100 1528 4c6a 6176 612f 6c61 6e672f53 7472 696e 673b 2956 0021 0005 00060000 0000 0002 0001 0007 0008 0001 00090000 001d 0001 0001 0000 0005 2ab7 0001b100 0000 0100 0a00 0000 0600 0100 00000100 0900 0b00 0c00 0100 0900 0000 2b000200 0200 0000 0b12 024c b200 032b b60004b1 0000 0001 000a 0000 000e 0003 00000003 0003 0004 000a 0005 0001 000d 00000002 000e

2.1 魔数与Class文件版本

Class 文件的第 1 - 4 个字节代表了该文件的魔数(Magic Number)。它唯一的作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件,其值固定是:0xCAFEBABE(咖啡宝贝)。如果一个 Class 文件的魔数不是 0xCAFEBABE,那么虚拟机将拒绝运行这个文件。

Class 文件的第 5 - 6 个字节代表了 Class 文件的次版本号(Minor Version),即编译该 Class 文件的 JDK 次版本号。

Class 文件的第 7 - 8 个字节代表了 Class 文件的主版本号(Major Version),即编译该 Class 文件的 JDK 主版本号。


java字节码文件的扩展名是( ) java字节码文件类型_数据_02


从上面的class文件中看到。对应的魔数为cafe babe.次版本 0000 主版本0034.也就是通过JDK1.8编译的

2.2 常量池

紧跟版本信息之后的是常量池信息,其中前 2 个字节表示常量池个数,其后的不定长数据则表示常量池的具体信息。从版本信息后面的内容为 001d。这个转换为十进制表示29。查class文件格式说明中的表格可知道:当前常量共计 29 -1 = 28个。


java字节码文件的扩展名是( ) java字节码文件类型_java_03


2.2.1 分析常量

从001d以后是常量信息。一共28个。根据上面的表可以看到每个常量都有一个tag用于表示常量的类型。占用1个字节。根据tag对应的具体值,在查表后看具体该常量后面有几属性,分别占用多少字节。由此可以看到第一个常量是

0a00 0600 0f

第一个字节0a对应的十进制是10。查表可以对应的是CONSTANT_Methodref_info常量。具体的信息如下根据结构整理后如下:

-- 第1个常量CONSTANT_Methodref_info { u1 tag = 0x0a; u2 class_index = 0x0006; u2 name_and_type_index = 0x000f;}-- 第2个常量CONSTANT_String_info { u1 tag = 0x08; u8 length = 0x0010;}-- 第3个常量CONSTANT_Fieldref_info { u1 tag = 0x09; u2 class_index = 0x0011 u2 name_and_type_index = 0x0012;}-- 第4个常量CONSTANT_Methodref_info { u1 tag = 0x0a; u2 class_index = 0x0013 u2 name_and_type_index = 0x0014;}-- 第5个常量CONSTANT_Class_info { u1 tag = 0x07; u2 utf8_index = 0x0015;}-- 第6个常量CONSTANT_Class_info { u1 tag = 0x07; u2 utf8_index = 0x0016;}-- 第7个常量CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0006; => 6个字节长度 u1 bytes = 0x3c696e69743e}-- 第8个常量CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0003; => 3个字节长度 u1 bytes = 0x282956 }-- 第9个常量CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0004; => 4个字节长度 u1 bytes = 0x436f6465 }-- 第10个常量CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x000f; => 15个字节长度 u1 bytes = 0x4c696e654e756d6265725461626c65 }-- 第11个常量CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0004; => 4个字节长度 u1 bytes = 0x6d61696e }-- 第12个常量CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0016 => 22个字节长度 u1 bytes = 0x285b4c6a6176612f6c616e672f537472696e673b2956 }-- 第13个常量CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x000a; => 10个字节长度 u1 bytes = 0x536f7572636546696c65 } -- 第14个常量CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0009; => 9个字节长度 u1 bytes = 0x546573742e6a617661} -- 第15个常量CONSTANT_NameAndType_info { u1 tag = 0x0c; u2 class_info_index = 0x0007; u1 filed_info_index = 0x0008 }-- 第16个常量 CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x000c; => 12个字节长度 u1 bytes = 0x48656c6c6f20576f6c72642e} -- 第17个常量 CONSTANT_Class_info { u1 tag = 0x07; u2 index = 0x0017; }-- 第18个常量CONSTANT_NameAndType_info { u1 tag = 0x0c; u2 class_info_index = 0x0018;  u1 filed_info_index = 0x0019 }-- 第19个常量 CONSTANT_Class_info { u1 tag = 0x07; u2 index = 0x001a; }-- 第20个常量CONSTANT_NameAndType_info { u1 tag = 0x0c; u2 class_info_index = 0x001b;  u1 filed_info_index = 0x001c; }-- 第21个常量 CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0004; => 4个字节长度 u1 bytes = 0x54657374 }-- 第22个常量 CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0010; => 10个字节长度 u1 bytes = 0x6a6176612f6c616e672f4f626a656374 } -- 第23个常量 CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0010; => 10个字节长度 u1 bytes = 0x6a6176612f6c616e672f53797374656d }-- 第24个常量 CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0003; => 3个字节长度 u1 bytes = 0x6f7574 }-- 第25个常量 CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0015; => 15个字节长度 u1 bytes = 0x4c6a6176612f696f2f5072696e7453747265616d3b } -- 第26个常量 CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0013; => 13个字节长度 u1 bytes = 0x6a6176612f696f2f5072696e7453747265616d }-- 第27个常量 CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0007; => 7个字节长度 u1 bytes = 0x7072696e746c6e } -- 第28个常量 CONSTANT_Utf8_info { u1 tag = 0x01; u2 lenght = 0x0015; => 15个字节长度 u1 bytes = 0x284c6a6176612f6c616e672f537472696e673b2956 }

2.2.2 常量具体的值

根据上面将每个常量对应的信息都标记出来后,可以很方便的按照对应的index进行组装,结果如下:

第1个常量:java/lang/Object()V 第2个常量为:Hello Wolrd.第3个常量为:java/lang/System out:Ljava/io/PrintStream;第4个常量为:java/io/PrintStream println(Ljava/lang/String;)V第5个常量为:Test第6个常量为:java/lang/Object第7个常量为:第8个常量为:()V第9个常量为:Code第10个常量为:LineNumberTable第11个常量为:main第12个常量为:([Ljava/lang/String;)V第13个常量为:SourceFile第14个常量为:Test.java第15个常量为:()V第16个常量为:Hello Wolrd.第17个常量为:java/lang/System第18个常量为:out:Ljava/io/PrintStream;第19个常量为:java/io/PrintStream第20个常量为:println(Ljava/lang/String;)V第21个常量为:Test第22个常量为:java/lang/Object第23个常量为:java/lang/System第24个常量为:out第25个常量为:Ljava/io/PrintStream;第26个常量为:java/io/PrintStream第27个常量为:println第28个常量为:(Ljava/lang/String;)V

通过javap来反编译,查看一下常量池具体信息:


java字节码文件的扩展名是( ) java字节码文件类型_java字节码文件的扩展名是( )_04


2.3 访问标志

在常量池结束之后,紧跟着的2个字节表示访问标识符:0x0021 访问标识符表格如下:


java字节码文件的扩展名是( ) java字节码文件类型_数据_05


由上面的表格可以看到,并没有0x0021的访问权限。是因为这里的访问标志可以有多个标志名称组成。代码中的访问标志是通过多个值进行或运算得到的结果

2.4 类索引、父类索引、接口索引

在访问标记后,则是类索引、父类索引、接口索引的数据,这里数据为:0x0005 0006 0000 。 类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class 文件中由这三项数据来确定这个类的继承关系

2.4.1 类索引

类索引在常量池中的索引值为0x0005,十进制为5,指向一个CONSTANT_Class_info类型的常量,其tag值为7,name_index指向21的索引。


java字节码文件的扩展名是( ) java字节码文件类型_java_06


2.4.2 父类索引

类索引之后,是父类索引。在常量池中的索引值为0x0006,十进制为6,指向一个CONSTANT_Class_info类型的常量,其tag值为7,name_index指向22的索引。


java字节码文件的扩展名是( ) java字节码文件类型_字节码_07


2.4.3 接口索引

接口索引集合就用来描述哪个类实现了哪些接口,这些被实现的接口将按 implements 语句后的接口顺序从左到右排列在接口索引集合中。对于接口索引集合,入口第一项是 u2 类型的数据为接口计数器(interfaces_count),表示索引表的容量,而在接口计数器后则紧跟着所有的接口信息。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节

Test类并没有实现任何接口,所以紧跟在父类索引后的两个自己是0x0000。这表示该类没有实现任何接口。因此后面的接口索引表为空

2.5 字段表集合

在接口索引表之后是字段索引计数器和字段索引表。字段索引计数器是一个u2类型的数值,class文件中的值为0x0000,表示有没有声明的变量。

字段表中的每项都表示指向常量池中的一个索引,该索引指向一个field_info结构的数据。字段表描述当前类或接口声明的所有字段,但不包括从父类或接口中继承过来的。

field_info的结构如下:


java字节码文件的扩展名是( ) java字节码文件类型_java_08


2.6 方法表集合

在字段表后的 2 个字节是一个方法计数器,标识当前类中一共有几个方法,在方法计数器之后,标识的为方法的详细数据。通过上面的字节码文件可以看到: 0x0002 表示有两个方法。方法表的信息如下:


java字节码文件的扩展名是( ) java字节码文件类型_java_09


通过方法表和字节码文件,对应的两个方法为。第一个方法如下:

0001 0007 0008 0001 0009 0000 001d

方法开始的前2个字节表示访问标识,这里是0x0001 。表示public。

紧接着是方法名索引,0x0007指向常量池中的第7个常量,根据上面的内容可以知道为: 。紧着是方法描述符索引。这里是0x0008,指向常量池中第8个常量,为:()V。

紧接着两个表示属性计数器,这里是0x0001。表示该方法一共有一个属性,属性表内容为:


java字节码文件的扩展名是( ) java字节码文件类型_java字节码文件的扩展名是( )_10


前两个字节是名字索引,紧接着4个字节是属性长度,后面是属性的值。属性名称为0x0009。查询上面的内容可以得到为Code。说明此属性是方法的字节码描述。Code属性的表结构如下:


java字节码文件的扩展名是( ) java字节码文件类型_值对于无符号的字节太大或太小_11


从这里开始可以看到,属性名称索引对应的为0x0009 ,也就是Code信息。紧接着4个字节长度表示属性长度。对应的0x0000001d。对应的值为29

属性长度后是栈深度,2个字节表示。为0x0001

栈深度后是局部变量所需存储空间,2个字节表示,为: 0x0001。表示局部变量表所需的存储空间为 1 个 Slot。在这里 max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位

之后是字节码的长度,为0x00000005。4个字节表示。这里得出之后的5个字节表示具体的字节码数据。具体的值为: 2ab7 0001 b1。

具体的字节码指令表为:

2a -> 查表可知为aload_0。表示将第一个本地变量推送到栈顶b7 -> invokespecial 调用超类构建方法, 实例初始化方法, 私有方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用00 -> nop 01 -> 将null推送至栈顶b1 -> return 含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束

异常表的长度为紧跟着的2个字节,这里为0x0000。也就是没有对应的异常表数据。

异常表的数据之后是属性表的信息,这个属性表的数据是:0x0001。表示有一个属性信息。属性表的结构如下


java字节码文件的扩展名是( ) java字节码文件类型_值对于无符号的字节太大或太小_12


前两个字节表示属性的名称, 这里值为0x000a。对应的为常量池中第10个常量。具体的为:LineNumberTable。这里LineNumberTable表结构的数据如下:


java字节码文件的扩展名是( ) java字节码文件类型_java_13


紧跟着的4个字节为属性数据的长度。这个为0x00000006。表示有6个字节的数据。紧接着的0x0001,表示长度为1。后面跟着的是line_number_table类型的数据。line_number_table暂用的分别是4个字节。前两个字节表示字节码行号,后两个自己为源码行号。可以通过字节码可以看到为 0x00000001。表示字节码第0行和源码第一行。

第一个方法就分析完了。对应的第一个方法的数据为:0001 0007 0008 0001 0009 0000 001d 0001 0001 0000 0005 2ab7 0001 b100 0000 0100 0a00 0000 0600 0100 0000 01

反编译源码查看分析结果:


java字节码文件的扩展名是( ) java字节码文件类型_字节码_14


第二个方法的解析类似

00 09 => 访问权限,这里是public static void00 0b => 方法名索引,常量表中第11个常量,为main00 0c => 方法描述符索引,常量表中第12个常量,为([Ljava/lang/String;)V00 01 => 属性表计数器,表示有1个属性00 09 => 属性名称索引,这里为Code00 0000 2b => 表示属性长度为4300 02 => 栈深度为200 02 => 局部变量存储的空为2个Slot00 0000 0b => 字节码长度为1112 024c b200 032b b600 04b1 => 具体的字节码指令0000 => 异常表长度为00001 => 属性表长度为1000a => 表示lineNumberTable0000 000e => 属性长度为140003 => line_number_table 为30000 0003=> 源码第3行对应字节码对0行0003 0004 => 源码第4行对应字节码第3行000a 0005 => 源码第5行对应字节码第10行

第二个方法对应的数据为

00 0900 0b00 0c00 0100 0900 0000 2b000200 0200 0000 0b12 024c b200 032b b60004b1 0000 0001 000a 0000 000e 0003 00000003 0003 0004 000a 0005

javap反编译结果截图:


java字节码文件的扩展名是( ) java字节码文件类型_字节码_15


2.7 属性表集合

这里的属性表集合是对应的勒种属性。紧接着下面的数据为

0001 000d 0000 0002 000e

详细的分析结果如下

0001 => 表示有1个属性000d => 为13,表示对应的为常量池中第13个常量。值为SourceFile0000 0002 => 属相长度占用4个字节。表示属性长度为2000e => 占用2个字节,长度为上面0000 0002的值。 对应常量池中第14个常量,为Test.java

属性截图为:


java字节码文件的扩展名是( ) java字节码文件类型_字节码_16


3 结果汇总

从整个分析过程来说,其实还是蛮简单的,不过要认真坚持一步一步分析下来。分析完以后对字节码文件也就有了初步的认识。一般也不会通过手动一步一步的分析。java提供了javap命令来支持。下面贴出javap编译的结果


java字节码文件的扩展名是( ) java字节码文件类型_字节码_17


字节码结果文件备注

魔数:cafe babe版本:0000 0034常量个数:001d 常量池:0a00 0600 0f080010 0900 1100 120a 0013 0014 0700 15070016 0100 063c 696e 6974 3e01 0003 28295601 0004 436f 6465 0100 0f4c 696e 654e756d 6265 7254 6162 6c65 0100 046d 61696e01 0016 285b 4c6a 6176 612f 6c61 6e672f53 7472 696e 673b 2956 0100 0a53 6f757263 6546 696c 6501 0009 5465 7374 2e6a6176 610c 0007 0008 0100 0c48 656c 6c6f2057 6f6c 7264 2e07 0017 0c00 1800 1907001a 0c00 1b00 1c01 0004 5465 7374 0100106a 6176 612f 6c61 6e67 2f4f 626a 65637401 0010 6a61 7661 2f6c 616e 672f 53797374 656d 0100 036f 7574 0100 154c 6a617661 2f69 6f2f 5072 696e 7453 7472 65616d3b 0100 136a 6176 612f 696f 2f50 72696e74 5374 7265 616d 0100 0770 7269 6e746c6e 0100 1528 4c6a 6176 612f 6c61 6e672f53 7472 696e 673b 2956 访问标志:0021 类索引,父类索引,接口索引:0005 0006 0000 字段表集合:0000 方法表集合:0002 0001 0007 0008 0001 00090000 001d 0001 0001 0000 0005 2ab7 0001b100 0000 0100 0a00 0000 0600 0100 00000100 0900 0b00 0c00 0100 0900 0000 2b000200 0200 0000 0b12 024c b200 032b b60004b1 0000 0001 000a 0000 000e 0003 00000003 0003 0004 000a 0005 属性表集合0001 000d 00000002 000e

4 字节码指令表


java字节码文件的扩展名是( ) java字节码文件类型_字节码_18