JVM系列第5讲:字节码文件结构原创
前面我们说到 Java 虚拟机使用字节码实现了跨平台的愿景,无论什么系统,我们都可以使用 Java 虚拟机解释执行字节码文件。但其实字节码是有一套规范的,而规定字节码格式的就是《Java 虚拟机规范》。《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 的表(常量池),这里面存储了该类的所有常量。
而整个字节码文件本质上就是一张表,它由下面几个部分组成:
为了便于理解,我将一个完整的表划分为以下七个部分,这七个部分组成了一个完整的 Class 字节码文件:
- 魔数与Class文件版本
- 常量池
- 访问标志
- 类索引、父类索引、接口索引
- 字段表集合
- 方法表集合
- 属性表集合
在开始之前,我们先写一个最简单的入门 Hello World。接下来我们将以这个 Hello World 文件编译后的字节码文件为例子,来解析字节码文件内容。
public class Demo{
public static void main(String args[]){
System.out.println("Hello World.");
}
}
接着在命令行运行javac Demo.java命令编译这个类,这时会生成一个 Demo.class 文件。
接着我们用纯文本编辑器打开生成的 Demo.class 文件。
cafe babe 0000 0034 001d 0a00 0600 0f09
0010 0011 0800 120a 0013 0014 0700 1507
0016 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0100 0a53 6f75
7263 6546 696c 6501 0009 4465 6d6f 2e6a
6176 610c 0007 0008 0700 170c 0018 0019
0100 0b48 656c 6c6f 2057 6f72 6c64 0700
1a0c 001b 001c 0100 0444 656d 6f01 0010
6a61 7661 2f6c 616e 672f 4f62 6a65 6374
0100 106a 6176 612f 6c61 6e67 2f53 7973
7465 6d01 0003 6f75 7401 0015 4c6a 6176
612f 696f 2f50 7269 6e74 5374 7265 616d
3b01 0013 6a61 7661 2f69 6f2f 5072 696e
7453 7472 6561 6d01 0007 7072 696e 746c
6e01 0015 284c 6a61 7661 2f6c 616e 672f
5374 7269 6e67 3b29 5600 2100 0500 0600
0000 0000 0200 0100 0700 0800 0100 0900
0000 1d00 0100 0100 0000 052a b700 01b1
0000 0001 000a 0000 0006 0001 0000 0001
0009 000b 000c 0001 0009 0000 0025 0002
0001 0000 0009 b200 0212 03b6 0004 b100
0000 0100 0a00 0000 0a00 0200 0000 0300
0800 0400 0100 0d00 0000 0200 0e
首先我们要清楚,字节码文件是使用十六进制进行编码的,而十六进制使用0x表示。接下来我们用上面「Hello World」的字节码文件为例子,一步步分析这七部分内容。
魔数与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 主版本号。
高版本的 JDK 能向下兼容以前笨笨的 Class 文件,但不能运行新版本的 Class 文件。例如一个 Class 文件是使用 JDK 1.5 编译的,那么我们可以用 JDK 1.7 虚拟机运行它,但不能用 JDK 1.4 虚拟机运行它。下表列出了各个版本 JDK 的十六进制版本号信息:
我们看看之前的 Demo 文件的 Class 文件内容,其前 8 个字节分别是:cafe babe 0000 0034。
对比上面表格中的数据,那么我们可以知道,这个 Class 文件是由 JDK1.8 编译的。
常量池
紧跟版本信息之后的是常量池信息,其中前 2 个字节表示常量池个数,其后的不定长数据则表示常量池的具体信息。
我们可以从上图知道,常量池的常量都是由cp_info这种表结构组成的,而且表结构不同其大小也不同。在 Java 虚拟机规范中一共有 14 种 cp_info 类型的表结构。
而上面这些 cp_info 表结构又有不同的数据结构,其对应的数据结构如下图所示。
cp_info表结构一共有三个字段,第一个字段表示这个表结构的标示值,有一个字节大小,对应我们上一个表格中的数字。第二、三个字段表示其表结构的描述,不同字段其意思不太一样。
看到这里可能有点犯模糊,这么些表格到底应该怎么用呢?没关系,我们举个例子就清楚了。
接下来我们继续看看 Hello World 字节码文件的内容。上一小节说到字节码文件的版本,那么接下来就是常量池的内容了。
Hello World 文件字节码对应的内容是:00 1d,其值为 29,表示一共有 29 - 1 = 28 个常量。
紧跟着常量池的就是 28 个常量了,因为每个常量都对应不同的类型,所以我们无法得知其具体大小,只能一个个分析。
第 1 个常量。紧接着 001d 的后一个字节为 0A,为十进制数字 10,查表可知其为方法引用类型(CONSTANT_Methodref_info)的常量。
再查 cp_info 对应的表结构知道,该常量项第 2 - 3 个字节表示类信息,第 4 - 5 个字节表示名称及类描述符。
接下来我们取出这部分的数据:0a 0600 000f。
该常量项第 2 - 3 个字节,其值为 00 06,表示指向常量池第 6 个常量所表示的信息。根据后面我们分析的结果知道第 6 个常量是 java/lang/Object。第 4 - 5 个字节,其值为 000f,表示指向常量池第 15 个常量所表示的信息,根据 javap 反编译出来的信息可知第 10 个常量是 <init>:()V。将这两者组合起来就是:java/lang/Object.<init>:V,即 Object 的 init 初始化方法。
大致就是按照上面的方式去分析每一个常量的值和意义,接下来我继续分析接下来的 27 个常量。
第 2 个常量,数据为 09 0010 0011。紧接着 000f 的后一个字节为 09,表示该常量为字段的符号引用(CONSTANT_Fieldref_info)。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示类信息,这里是 0010 表示指向常量池第 16 个常量所表示的信息,根据 javap 反编译我们知道其是 java/lang/System.out。该常量项的第 4 - 5 个字节表示名称及类描述符,这里值为 0011 表示指向常量池第 17 个常量所表示的信息,javap 反编译得知是 Ljava/io/PrintStream。结合起来就是:java/lang/System.out:Ljava/io/PrintStream;
第 3 个常量,数据为 08 00 12。紧接着 0011 的后一个字节为 08,表示该常量为字符串引用类型(CONSTANT_String_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示指向字符串字面量的索引,这里是 0012 表示指向常量池的第 18 个常量。javap 反编译得知其是一个Hello World!字符串。
第 4 个常量,数据为 0A 0013 0014。紧接着 0012 的后一个字节为 0A,表示该常量为方法引用类型(CONSTANT_MethodHandle_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示类信息,这里是 0013 表示指向常量池第 19 个常量所表示的信息。该常量项的第 4 - 5 个字节表示名称及类描述符,这里值为 0014 表示指向常量池第 20 个常量所表示的信息。结果是:java/io/PrintStream.println:(Ljava/lang/String;)V。
第 5 个常量,数据为 07 00 15。紧跟着 0014 后的是 07,表示是类信息类型常量,表结构如下。该表后紧跟着一个 2 个字节的索引,这里是 0015,其指向了常量池第 21 个常量,反编译得知其值为Demo。
第 6 个常量,数据为 07 0016。07 表示其是类信息类型常量,其指向了常量池第 22 个常量。从后边的分析可以知道,第 22 个常量为字符串java/lang/Object。
第 7 个常量,数据为 01 0006 3C 69 6E 69 74 3E。其中 01 表示其是字符串(CONSTANT_Utf8_info)的常量,0006 表示其字符串长度为 6 个字节。随后跟着的 3C 69 6E 69 74 3E 为字符串的值。在 Class 文件中,字符串是使用 ASCII 码进行编码的,我们将这些十六进制字符转换成对应的 ASCII 码之后,其值为:<init>。
第 8 个常量,数据为 01 00 03 28 29 56。其中 01 表示其是字符串(CONSTANT_Utf8_info)的常量,0003 表示其字符串长度为 3 个字节。随后跟着的 28 29 56 为字符串的值。在 Class 文件中,字符串是使用 ASCII 码进行编码的,我们将这些十六进制字符转换成对应的 ASCII 码之后,其值为:()V。
第 9 个常量,数据为 01 00 04 43 6f 64 65。它是一个字符串常量,转换之后是:Code。
第 10 个常量,数据位 01 00 0f 4c 696e 654e 756d 6265 7254 6162 6c65 是一个字符串常量,转换之后是:LineNumberTable。
第 11 个常量,数据为 01 00 04 6d 6169 6e。它是一个字符串常量,转换之后是:main。
第 12 个常量,数据为 01 0016 285b 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 2956 是一个字符串常量,转换之后是:([Ljava/lang/String;)V。
第 13 个常量,数据为 01 00 0a 53 6f75 7263 6546 696c 65。它是一个字符串常量,转换之后是:SourceFile。
第 14 个常量,数据为 01 0009 4465 6d6f 2e6a 6176 61。它是一个字符串常量,转换之后是:Demo.java。
第 15 个常量,数据为 0c 0007 0008。这里表示 tag 的值是 0C,表示该常量为方法引用类型(CONSTANT_NameAndType_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示字段或方法名的索引,这里是 0007 表示指向常量池第 7 个常量所表示的信息,即<init>。该常量项的第 4 - 5 个字节表示字段或方法描述符的索引,这里值为 0008 表示指向常量池第 8 个常量所表示的信息,即()V。所以第 15 个常量表示的信息其实是:"<init>":()V。
第 16 个常量,数据为 07 00 17。这里表示 tag 的值是 07,表示该常量为类信息类型(CONSTANT_Class_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示全限定名常量项的索引,这里是 0017 表示指向常量池第 23 个常量所表示的信息,即 java/lang/System。
第 17 个常量,数据为 0c 0018 0019。这里表示 tag 的值是 0C,表示该常量为方法引用类型(CONSTANT_NameAndType_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示字段或方法名的索引,这里是 0018 表示指向常量池第 24 个常量所表示的信息,即 out。该常量项的第 4 - 5 个字节表示字段或方法描述符的索引,这里值为 0019 表示指向常量池第 25 个常量所表示的信息,即Ljava/io/PrintStream;。所以第 17 个常量表示的信息其实是:out:Ljava/io/PrintStream;。
第 18 个常量,数据为 01 00 0b 48 656c 6c6f 2057 6f72 6c64 。它是一个字符串常量,转换之后是:Hello World。
第 19 个常量,数据为 07 001a。这里表示 tag 的值是 07,表示该常量为类信息类型(CONSTANT_Class_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示全限定名常量项的索引,这里是 001A 表示指向常量池第 26 个常量所表示的信息,即java/io/PrintStream。
第 20 个常量,数据为 0c 001b 001c 。这里表示 tag 的值是 0C,表示该常量为方法引用类型(CONSTANT_NameAndType_info)的常量。从上面的总表查阅知道,该常量项第 2 - 3 个字节表示字段或方法名的索引,这里是 001B 表示指向常量池第 27 个常量所表示的信息,即println。该常量项的第 4 - 5 个字节表示字段或方法描述符的索引,这里值为 001C 表示指向常量池第 28 个常量所表示的信息,即(Ljava/lang/String;)V。所以这里第 20 个常量的值为 println:(Ljava/lang/String;)V。
第 21 个常量,数据为 01 00 04 44 656d 6f。是一个字符串常量,转换之后是:Demo。
第 22 个常量,数据为 01 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374。是一个字符串常量,转换之后是:java/lang/Object。
第 23 个常量,数据为 01 00 10 6a 6176 612f 6c61 6e67 2f53 7973 7465 6d。是一个字符串常量,转换之后是:java/lang/System。
第 24 个常量,数据为 01 0003 6f75 74。是一个字符串常量,转换之后是:out。
第 25 个常量,数据为 01 0015 4c6a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d 3b。是一个字符串常量,转换之后是:Ljava/io/PrintStream;。
第 26 个常量,数据为 01 0013 6a61 7661 2f69 6f2f 5072 696e 7453 7472 6561 6d。是一个字符串常量,转换之后是:java/io/PrintStream。
第 27 个常量,数据为 01 0007 7072 696e 746c 6e。是一个字符串常量,转换之后是:println。
第 28 个常量,数据为 01 0015 284c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b29 56。是一个字符串常量,转换之后是:(Ljava/lang/String;)V。
到这里,我们常量池里 28 个常量已经全部解析完了。我们通过手动分析,了解了常量池的构成,但很多时候我们可以借助 JDK 提供的 javap 命令直接查看 Class 文件的常量池信息。
当我们运行javap -verbose Demo.class时,控制台会打印出该 Class 文件的构成信息,其中就包括了常量池的信息。
将利用 javap 打印出的结果,与我们手动分析的结果对比一下,你会发现结果是一致的。
访问标志
在常量池结束之后,紧接着的两个字节代表类或接口的访问标记(access_flags)。这里的数据为 00 21。
这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口、是否定义为public类型、是否定义为abstract类型等。具体的标志位以及标志的含义见下表。
在这里这两个字节是 00 21,通过查看我们并没有发现有标志值是 00 21 的标志名称。这是因为这里的访问标志可能是由多个标志名称组成的,所以字节码文件中的标志值其实是多个值进行或运算的结果。
通过查阅上述表格,我们可以知道,00 21 由 00 01(第1行)和 00 20(第3行)进行或运算得来。也就是说该类的访问标志是 public 并且允许使用 invokespecial 字节码指令的新语义。
类索引、父类索引、接口索引
在访问标记后,则是类索引、父类索引、接口索引的数据,这里数据为:00 05 00 06 00 00。
类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class 文件中由这三项数据来确定这个类的继承关系。
类索引。类索引用于确定这个类的全限定名,它用一个 u2 类型的数据表示。这里的类索引是 00 05 表示其指向了常量池中第 5 个常量,通过我们之前的分析,我们知道第 5 个常量其最终的信息是 Demo 类。
父类索引。父类索引用于确定这个类的父类的全限定名,父类索引用一个u2类型的数据表示。这里的父类索引是 00 06 表示其指向了常量池中第 6 个常量,通过我们之前的分析,我们知道第 6 个常量其最终的信息是 Object 类。因为其并没有继承任何类,所以 Demo 类的父类就是默认的 Object 类。
接口索引。接口索引集合就用来描述哪个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身就是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。对于接口索引集合,入口第一项是 u2 类型的数据为接口计数器(interfaces_count),表示索引表的容量,而在接口计数器后则紧跟着所有的接口信息。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。
这里 Demo 类的字节码文件中,因为并没有实现任何接口,所以紧跟着父类索引后的两个字节是0x0000,这表示该类没有实现任何接口。因此后面的接口索引表为空。
字段表集合
字段表集合用于描述接口或者类中声明的变量,这里的数据为:00 00。
这里说的字段包括类级变量和实例级变量,但不包括在方法内部声明的局部变量。在类接口集合后的2个字节是一个字段计数器,表示总有有几个属性字段。在字段计数器后,才是具体的属性数据。
字段表的每个字段用一个名为 field_info 的表来表示,field_info 表的数据结构如下所示:
因为我们并没有声明任何的类成员变量或类变量,所以在 Demo 的字节码文件中,字段计数器为 00 00,表示没有属性字段。
方法表集合
在字段表后的 2 个字节是一个方法计数器,表示类中总有有几个方法,在字段计数器后,才是具体的方法数据。这里数据为:00 02 。
方法表中的每个方法都用一个 method_info 表示,其数据结构如下:
Demo 类的字节码文件中,方法计数器的值为 00 02,表示一共有 2 个方法。
第 1 个方法,这里数据为:00 01 00 07 00 08 00 01 00 09 00 0000 1d 00 01 00 01 00 0000 05 2a b7 00 01 b1 0000 0001 000a 0000 0006 0001 0000 0001。方法计数器后 2 个字节表示方法访问标识,这里是 00 01,表示其实 ACC_PUBLIC 标识,对比上面的图表可知其表示 public 访问标识。紧接着 2 个字节表示方法名称的索引,这里是 00 07 表示指向了常量池第 7 个常量,查阅可知其指向了<init>。紧接着的 2 个字节表示方法描述符索引项,这里是 00 08 表示指向了常量池第 8 个常量,查阅可知其指向了()V。
紧接着 2 个字节表示属性表计数器,这里是 00 01 表示该方法的属性表一共有 1 个属性。属性表的表结构如下:
前两个字节是名字索引、接着 4 个字节是属性长度、接着是属性的值。这里前两个字节为 0009,指向了常量池第9个常量,查询可知其值为Code,说明此属性是方法的字节码描述。 Code 属性的表结构如下:
根据 Code 属性对应表结构知道,前 2 个字节为 0009,即常量池第 9 个常量,查询知道是字符串常量Code。接着 4 个字节表示属性长度,这里值为 1D,即 29 的长度。下面我们继续分析 Code 属性的数据内容。
紧接着 2 个字节为 max_stack 属性。这里数据为 00 01,表示操作数栈深度的最大值。
紧接着 2 个字节为 max_locals属性。这里是数据为 00 01,表示局部变量表所需的存储空间为 1 个 Slot。在这里 max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。
接着 4 个字节为 code_length,表示生成字节码这里给的长度。这里数据为 00 00 00 05,表示生成字节码长度为 5 个字节。那么紧接着 5 个自己就是对应的数据,这里数据为 2a b7 00 01 b1,这一串数据其实就是字节码指令。通过查询字节码指令表,可知其对应的字节码指令:
- 读入2A,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
- 读入B7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。
- 读入00 01,这是invokespecial的参数,查常量池得0x0001对应的常量为实例构造器“”方法的符号引用。
- 读入B1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。
接着 2 个字节为异常表长度,这里数据为 00 00,表示没有异常表数据。那么接下来也就不会有异常表的值。
紧接着 2 个字节是属性表的长度,这里数据为 00 01,表示有一个属性。该属性长度为一个 attribute_info 那么长。attribute_info 属性表的表结构如下。
首先,前两个字节表示属性名称索引,这里数据为:00 0A。指向了第 10 个常量,查阅可知值为:LineNumberTable。LineNumberTable 表的表结构如下图所示。
其前两个字节是属性名称索引,就是上面已经分析过的 00 0A。
接着 4 个字节是属性长度,这里数据为 00 00 00 06,表示有 6 个字节的数据。接着 2 个字节是 LineNumberTable 的长度,这里数据是 00 01,表示长度为 1。接着跟着 1 个 line_number_info 类型的数据,下面是 line_number_info 表的结构,其包含了 start_pc 和 line_number 两个 u2 类型的数据项。前者是字节码行号,后者是 Java 源码行号。
那么接下来 2 个字节为 00 00,即 start_pc 表示的字节码行号为第 0 行。接着 00 01,即 line_number 表示 Java 源码行号为第 1 行。
到此,我们方法表集合的第一个方法分析结束。我们通过 javap 反编译查看,可以看到 Code 和 LineNumberTable 都是完全正确的。
接下来分析第 2 个方法。第二个方法的数据为:TODO。
前 2 个字节为方法访问标识,这里数据为 00 09,标识方法标识符为 public static void。
接着 2 个字节为方法名称索引项,这里数据为 00 0b,即常量池第 11 个常量,查询可知其值是main。
接着 2 个字节为方法描述符索引项,这里数据为 00 0c,即常量池第 12 个常量,查询可知其值是([Ljava/lang/String;)V。
接着 2 个常量标识属性表的数量,这里数据为 00 01,表示后面有 1 个类型为 表结构为 attribute_info 的属性信息。attribute_info 表的表结构如下。
即紧接着 2 个字符表示属性名的索引项,这里数据为 00 09,即对应常量池第 9 个常量,查询可知其值为:Code。Code 属性的表结构如下图所示。
Code 属性前 2 个字节表示其名字,这里分析过了,是Code。
接着 4 个字节表示属性的长度,这里数据是 00 00 00 25,表示长度为 37。
紧接着 2 个字节为 max_stack 属性。这里数据为 00 02,表示操作数栈深度的最大值为 2,其实是说有两个局部变量。
紧接着 2 个字节为 max_locals属性。这里是数据为 00 01,表示局部变量表所需的存储空间为 1 个 Slot。在这里 max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。
接着 4 个字节为 code_length,表示生成字节码这里给的长度。这里数据为 00 00 00 09,表示生成字节码长度为 9 个字节。那么紧接着 9 个自己就是对应的数据,这里数据为 b2 00 02 12 03 b6 00 04 b1,这一串数据其实就是字节码指令。通过查询字节码指令表,可知其对应的字节码指令。上面分析过一次了,这里就不再分析了。
接着 2 个字节为异常表长度,这里数据为 00 00,表示没有异常表数据。那么接下来也就不会有异常表的值。
紧接着 2 个字节是属性表的长度,这里数据为 00 01,表示有一个属性。该属性长度为一个 attribute_info 那么长。attribute_info 属性表的表结构如下。
首先,前两个字节表示属性名称索引,这里数据为:00 0A。指向了第 10 个常量,查阅可知值为:LineNumberTable。LineNumberTable 表的表结构如下图所示。
其前两个字节是属性名称索引,就是上面已经分析过的 00 0A。
接着 4 个字节是属性长度,这里数据为 00 00 00 0A,表示有 10 个字节的数据。接着 2 个字节是 LineNumberTable 的长度,这里数据是 00 02,表示长度为 2。接着跟着 2 个 line_number_info 类型的数据,下面是 line_number_info 表的结构,其包含了 start_pc 和 line_number 两个 u2 类型的数据项。前者是字节码行号,后者是 Java 源码行号。
第 1 个 line_number_info,即接下来 2 个字节为 00 00,即 start_pc 表示的字节码行号为第 0 行。接着 00 03,即 line_number 表示 Java 源码行号为第 3 行。
第 2 个 line_number_info,即接下来 2 个字节为 00 08,即 start_pc 表示的字节码行号为第 8 行。接着 00 04,即 line_number 表示 Java 源码行号为第 4 行。
这里的每个 line_number_info 占用 4 个字节,两个 line_number_info 一共 8 个字节。再加上表示 line_number_info 数量的 2 个字节,一共 10 个字节。刚好就与 attribute_length 的 00 00 00 0A 数据吻合。
到这里,第 2 个方法也分析结束了。同样我们通过javap命令反编译看看,会发现反编译的结果与我们分析的完全吻合,这说明我们的分析是正确的。
属性表集合
这里或许有人会迷惑,上面我们不是分析过属性表了么。其实上面分析的是方法中的属性,而这个是类中的属性。这个就像局部变量和类成员变量一样,是不同的。
紧接着我们剩下的数据为:00 0100 0d00 0000 0200 0e,这些就是属性表集合的数据了。
根据上面的表格我们知道,紧跟着的 2 个字节数据是属性表属性数量,这里数据为 00 01,表示有 1 个属性。后面紧跟着 1 个表结构为 attribute_info 的属性数据。attribute_info 表的结构如下图所示。
前两个字段为属性名称索引,这里数据为 00 0d,表示第 13 个常量池,查询可知这里的值是:SourceFile。SourceFile 属性的表结构如下图所示。
SourceFile 表结构前两个字节我们已经分析过,数据为 00 0d,表示第 13 个常量池,指的是SourceFile这个值。接着我们看后面 4 个字节,这里数据为 00 00 00 02,表示属性长度为 2 个字节。紧跟着的 2 个字节表示 SourceFile 的常量池索引,即该字节码文件的源文件名称,这里数据是 00 0e,即常量池的第 14 项,即Demo.java。所以这个属性项标识了该字节码文件的源文件名称为 Demo.java。
我们通过 javap 反编译一下,可以发现与我们的分析完全一致。
到这里,我们就从头到尾将字节码文件的每个字节的数据分析完毕。通过这么一次分析,相信大家对于字节码文件的构成已经了然于胸了。这样的分析非常耗费时间,但是确实对字节码结构最好的一次学习。有时候最笨的方法,恰恰是最高效的方法。还没坚持下来的同学,要至少坚持独立分析一次,这样的收获是很大的。
但在实际使用或分析问题的时候,我们通常用 javap 工具帮助我们完成这个过程,这样能提高效率。使用 javap 工具很简单,只需要这样使用:javap -verbose Demo.class 就可以将字节码文件全部分析出来。下面给出此次 Demo.class 文件的反编译完整截图。
总结
到这里我们通过对 Hello World 的解析,从而对 Java 类文件结构有了一个全面的认识。进一步还简单了解了 Java 虚拟机以及 Java 虚拟机规范。希望读完这篇文章,大家能对 Java 类文件结构有一个深入的认识。