Java的技术体系包括
- 支持Java程序运行的虚拟机(JVM)
- 提供接口支持的Java API
- Java 编程语言
- 第三方Java框架(如Spring等)
代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,确实编程语言的一大步。
随着计算机的诞生和发展,程序也随之产生并迅速发展起来。从最开始的机器语言,到汇编语言,再到高级语言,无论我们采用何种语言编写的程序,程序最终都需要经过编译器翻译成0和1构成的二进制文件,计算机才能够执行。然而随着虚拟机以及大量建立在虚拟机之上的程序语言的蓬勃发展,越来越多的程序语言已经不再需要把程序直接编译成二进制的本地机器码,而是通过编译成虚拟机能够识别的与具体操作系统和机器指令集无关的字节码,并将字节码运行在虚拟机上,从而使得程序的跨平台性变得轻而易取,真正实现一次编写,到处运行“Write Once,Run Anywhere”。
字节码格式与平台无关性
各种不同平台的虚拟机与所有平台都统一使用的程序存储格式————字节码,是构成平台无关性的基石。 同时Java虚拟机不仅可以支持Java程序的运行,他还支持别的语言,诸如Groovy,JRuby等只要这些语言编译后形成的字节码符合Java虚拟机规范的要求即可,这便虚拟机的另一种特性,语言无关性。
平台无关性和语言无关性的基础便是————虚拟机和字节码存储格式。Java虚拟机只与“Class文件”这种特定的二进制文件格式所关联,class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息,同时还有出于安全性考虑的很多强制性的语言和结构化约束。因此任何一种语言都可以通过相应的编译器编译成class文件,并在Java虚拟机上运行。例如,Java语言的各种变量,关键字,运算符号的语义,最终都要由多条字节码命令组合而成。
Class类文件结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
Class文件中只有两种数据类型:无符号数和表。
- 无符号数属于基本的数据类型,以u1,u2,u4,u8分别代表1个字节,2个字节,4个字节和8个字节的无符号数,它可以用来描述数字,索引引用,数量值,或者按照UTF8编码构成的字符串值。
- 表,由多个无符号数或者其他表作为数据项构成的。所有表都习惯性以 “info” 结尾。用于描述有层次关系的复合结构数据。
Class文件不像XML等描述性语言,由于它没有任何的分隔符,所以Class文件的数据项,哪个字节代表什么含义,长度是多少,先后顺序如何,都是被严格限定不允许被改变的。Class文件的格式如下表所示
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
从上表我们可以看出,当需要描述同一类型的多个数据时,会在其前面放置一个字段表示数据的数量。下面是Class文件中各个数据项的具体含义。
魔数
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
Class文件的头四个字节被称为魔数,它的唯一作用是确定这个文件是否为一个能够被虚拟机接受的Class文件。Class文件的魔数似乎颇有浪漫气息,其值为0xcafebabe。(这和日后出现Java商标相似)
Class文件版本
类型 | 名称 | 数量 |
---|---|---|
u2 | minor_version | 1 |
u2 | major_version | 1 |
紧接着魔数的两个数据项便是Class文件的版本号,次版本号和主版本号。Java的版本号从45开始,例如JDK1.7对应的版本号为0x0033(十进制为51)。JDK能支持以前版本的Class文件,但不能运行以后版本的Class文件。
常量池
类型 | 名称 | 数量 |
---|---|---|
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
紧接着主次版本号的是常量池,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项关联最多的数据类型,也是占用Class文件空间最大的数据项之一,同时也是Class文件中第一个出现的表类型数据项。
常量池中主要存放两大类常量:字面量、符号引用。
- 字面量比较接近Java语言层面的概念,如文本字符串、声明为final的常量值等。
- 符号引用则属于编译原理方面的概念,包括下面三类常量
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
常量池中每一项常量都是一个表,JDK1.7之前共有11种结构各不相同的表结构,在1.7之后为了更好地支持动态语言调用,又额外增加了三种。这14种表都有一个共同特点,就是表开始的第一位是一个u1类型的标志位(tag),用来表示当前的表属于哪种常量类型。这14种常量类型所表示的具体含义,如下表所示
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Interger_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或者接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
这14种常量类型各自都有自己的结构。例如关于“类或者接口的符号引用”的常量,上表可知为7,属于CONSTANT_Class_info类型,它的结构如下
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
其中tag表示该常量的标志位,属于哪一种常量。name_index是一个索引值,它指向常量池中的第name_index个常量,此常量通常而言是一个CONSTANT_Utf8_info类型的常量,代表了这个类的全限定名。而CONSTANT_Utf8_info类型常量的结构如下
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
其中tag依然表示该常量的类型,length表示这个UTF-8编码的字符串长度是多少个字节,后面的bytes是一个长度为length个字节的字符串。
此处我们可以看到,在Java类中我们编写的类名(类全限定名),字段名,方法名,字符串等编译成Class文件之后,都需要使用CONSTANT_Utf8_info类型来表示,CONSTANT_Utf8_info类型最大的长度是一个u2类型所表示的最大数,也就是两个字节的最大数65535。所以Java字符串的最大长度为65535个字节,否则将无法编译。
同时,我们可以借助jdk中的javap命令帮助我们分析Class文件,其中便会分析出常量池中的内容。命令如下
javap -v TestClass.class
访问标志
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
常量池之后,紧接着的两个字节代表访问标志access_flags,该字段的作用在于识别一些类或者接口层次的访问信息。比如该Class文件是类还是接口,是否是public,是否是abstract类型,是否声明final等。该字段有两个字节,16位,虚拟机规范采用按位标志的方式,总共可以使用16个标志位,不过当前只定义了其中8位。具体的标志位及其含义如下表所示
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语意,(历史原因导致)JDK1.0.2之后编译出来的类这个标志都为真 |
ACC_INTERFACE | 0x0200 | 是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口和抽象类来说,此标志为真,其余均为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
类索引,父类索引,接口索引集合
类型 | 名称 | 数量 |
---|---|---|
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
类索引this_class,父类索引super_class,都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合。Class文件中用这三项数据来表示这个类的继承关系。类索引表示这个类的全限定名,父类索引表示着该类的父类的全限定名,他们都是采用两个字节的索引值表示,索引值为指向常量池中的第几个常量。对于接口索引,第一项为两个字节的计数器,表示其后有多少个接口。如果计数器值为0,后面的接口索引不再占用任何字节。
字段表集合
类型 | 名称 | 数量 |
---|---|---|
u2 | fields_count | 1 |
field_info | fields | fields_count |
字段表field_info用于描述类或者接口中的变量。其中包括了类变量和实例变量(但不包括方法中声明的局部变量)。在Java语言中,描述一个类中的变量,可以包括的信息有很多,比如,作用域(public)、是否静态(static)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、变量类型(int [] 类)、变量名等。上述信息中,修饰符要么有要么没有,很适合采用标志位来表示,但是变量类型和变量名是千变万化的,所以需要引用常量池中的常量来描述。下表为字段表的结构
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attributes_info | attributes | attribute_count |
其中字段修饰符access_flags与类的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_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生 |
ACC_ENUM | 0x4000 | 字段是否为enum |
其后的两项索引值name_index表示字段的简单名称,即我们通常所说的变量名称。descriptor_index表示字段的描述符,也就是该变量是什么类型。类型包括基本数据类型、方法无返回值的void类型,以及对象类型。类型的表示与含义如下图所示。
标志字符 | 含义 | 标志字符 | 含义 | |
---|---|---|---|---|
B | 基本类型byte | J | 基本类型long | |
C | 基本类型char | S | 基本类型short | |
D | 基本类型double | Z | 基本类型boolean | |
F | 基本类型float | V | 特殊类型void | |
I | 基本类型int | L | 对象类型,如Ljava/lang/Object |
如上表所示,基本类型和void都用大写字符来表示,对象类型则采用L加全限定名来表示。对于数组,每一个维度将使用一个前置的 [ 字符来描述,比如 String [][] 类型的二维数组,被表示为[[Ljava/lang/String;
。描述符之后紧跟的是属性表集合,用来表示一些额外的信息。
方法表集合
类型 | 名称 | 数量 |
---|---|---|
u2 | methods_count | 1 |
method_info | methods | methods_count |
字段表之后紧跟的就是方法表集合,存储格式与字段表集合基本相同。方法表的结构图如下
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attributes_info | attributes | attribute_count |
方法表和字段表在访问标志access_flags有一些区别,区别为volatile和transient只能修饰字段,而synchronized, native, abstract只能修饰方法,因此方法表的访问标志如下表所示
标志名称 | 标志值 | 含义 |
---|---|---|
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_BRIDGE | 0x0040 | 字段是否为编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICTFP | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生 |
方法的名称索引和描述符索引与字段表对应索引含义是一致的,上面已经解释过。而方法的属性表则是方法的重中之重,方法中的Java代码,在经过编译器编译后,成为字节码指令,这些字节码指令就存在方法属性表集合中名为Code的属性当中。
字段表和方法表中都不会出现父类的字段和方法,不过,有可能出现编译器自动添加的字段或者方法,例如内部类会添加外部类索引的字段,编译器会自动添加类构造器
或者实例构造器 方法。
属性表集合
类型 | 名称 | 数量 |
---|---|---|
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
属性表(attribute_info)在前面已经出现过多次,Class文件总结构,字段表,方法表都可以有自己的属性表集合,用来描述相应场景下专有的信息。
与Class文件中其他数据项有严格的顺序、长度、内容不同,属性表集合的限制稍微宽松了些,不再要求各个属性表有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,虚拟机运行时会忽略掉它不认识的属性。
对于每个属性而言,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来标识,而属性的结构则是完全自定义的,只需要通过一个u4的数据去说明属性值所占的位数即可。一个符合规范的属性表结构如下所示
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
属性表的类型非常之多,虚拟机规范第七版中已经增加到21项,因此下一篇文章详细介绍一些重点的属性。
总结
上面对class文件的总体结构进行了逐一的讲解,依然比较抽象,那么下图是对class文件结构的形象生动的描绘,相信一定能让我们理解起来更为容易。