摘要
- Java字节码的二进制格式
- Java字节码的魔数与版本
- Java字节码的常量池
- Java字节码的类继承
- Java字节码的字段存储
- Java字节码的方法格式
要想深刻理解JVM执行引擎的机制,就必须对JVM内部的数据结构有深入了解,而要了解JVM内部的数据结构,就必须要了解从Java源程序过渡到JVM内部数据结构的中间桥梁——Java字节码。
字节码格式
首先准备一个测试用例。
|
|
这个测试类虽然简单,然而五脏俱全,包含类变量、成员变量、字符串和类成员方法,同时包含一个入口函数main(),并且在main()函数中实例化了Test类。
接着编译Test.java类,得到Test.class文件。进入Test.java文件所在目录,输入如下命令:
|
|
执行javap命令后将输出如下信息:
|
|
使用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类型来获取该元素的具体类型。常量池的结构组成如下图所示。
使用结构化的方式来描述常量池数组的编排,可以如下描述:
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类型的属性名称指向常量池中对应的元素。