Java语言在刚刚诞生的时候提出过一句著名的口号“一次编写,到处运行”,这句话充分的表达了开发人员对于冲破平台界限的渴望,也解释了Java语言跟平台无关的设定。
一、 class文件意义
众所周知,Java语言是编译型语言,如果要执行Java代码,则首先需要将源码进行编译,变成虚拟机字节码文件,然后由虚拟机执行字节码文件,字节码文件和虚拟机才是Java语言无关平台的关键,本文将简要介绍此字节码文件结构。
class文件是一组以8位字节为基础的二进制流,各个数据项按照严格的顺序排列在class文件中,中间没有任何分隔符,所以整个class文件都是程序运行的必要数据,没有空隙存在,当遇到大于8位字节的数据项时,则会按照高位在前的排列方式分割成若干个8位字节进行存储。不同版本的虚拟机编译的class文件也不同,笔者所用jdk版本为1.8.0为例,介绍class类文件。
二、 class文件存储形式
根据Java虚拟机规定,class文件以类似于C语言的伪结构存储数据,这种伪结构中只有两种数据:无符号数和表,文件中的所有内容都以这两种数据结构存储。
无符号数属于基本的数据类型,以u1、u2、u4、u8来代表1个字节,2个字节、4个字节和8个字节构成的无符号数,无符号数可以用来描述数字、索引、数量值或者按照UTF-8编码的字符串。
表是有无符号数或者其他表组成的复杂数据类型,所有表都以 “_info”结尾。表用于描述有层次关系的复合结构数据,整个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 | attribute_count | 1 |
attribute_info | attributes | attributes_count |
三、 魔数
每个class文件的开头四个字节称为魔数(Magic Number),它的作用是标志这个class文件类型,能否被虚拟机正确解析。这种方式类似于文件的后缀,比如".jpg"、".xls"等,class文件没有采用后缀来识别class文件,主要是从安全方面考虑的,因为文件后缀是可以随意更改的,class文件的魔数为"CAFEBABE" 。
四、 版本号
紧接着魔数的四个字节为class文件的版本号,其中前两个字节代表次版本号(Minor Version),后两个字节代表主版本号(Major Version),java的版本号是从45开始计算的,比如jdk1.0使用45.0表示,jdk1.7.1使用51.1表示,每个大版本发布,主版本号加一。
为了便于介绍,小编写了一段代码为例,代码如下图所示。这段代码以jdk1.6版本编译,使用winhex打开编译后的class文件,可以看到前四个字节为"CAFEBABE",次版本号为"00",主版本号为16进制的"32",转换为10进制,即50,代表jdk1.6。如下图所示:
package com.tgb.lawyer.test;
public class TestClass {
private int x;
public int add() {
return x++;
}
}
五、 常量池
紧接着主次版本号的内容为常量池,常量池在整个class文件中是篇幅最多的内容,也是与其他内容关联最多的数据类型。每个class文件的常量池都是不同的,所以常量池的入口需要使用一个常量池计数器(constant_pool_cont)来标志常量池中敞亮的数量。与其他集合类型数据(接口索引集合、字段集合、方法集合等)不同,常量池需要标志没有引用任何常量池,这里使用0来表示,所以常量池的数量表示是从1开始的,其他集合类型都是从0开始的,例如常量池数量22代表常量池中有21项常量。
常量池中主要存放两种类型数据:字面量(Literal)和符号引用(Symbolic References),字面量及字符、常量等;符号引用包含三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。Java代码在进行编译的时候,没有"连接"这个步骤,所以在编译器不会确定各个常量的内存入口地址,因此常量池中会保存各个方法、字段的描述信息,当虚拟机运行时再从常量池中获取符号引用,所以常量池是跟其他内容关联最多的数据类型。
常量池中每一项都是一个表,所有常量项名称均以"_info"结尾,我们以jdk1.7为例,jdk1.7共提供了14中表结构常量常量池结构如下图所示:
对照常量表继续解析之前的class文件,版本号之后为常量项,常量池容量为十六进制"16",十进制为22,总共21项常量。
第一个常量为十六进制"07",十进制为7,找到常量项tag为7的结构,是一个CONSTANT_Class_info,查看此常量结构,构造比较简单,为一个u1的tag标识符和一个u2类型的name_index,name_index是一个索引,指向指向另外常量,十六进制表示为"0002",十进制为2,代表第二项常量,tag位为"01",查看tag位01的常量,此常量为CONSTANT_Utf8_info结构,包括一个u1类型的tag、u2类型的length、u1类型的bytes,查看bytes内容十六进制为"001D",十进制为29,往后查找29个字节,十六进制为"63 6F 6D 2F 74 67 62 2F 6C 6E 77 79 65 72 2F 74 65 73 74 2F 54 65 73 74 43 6C 61 73 73",内容为"com/tgb/lawyer/test/TestClass",至此,第二项和第二项常量解析完成。
第三项常量tag为"07",十进制为7,依然是一个CONSTANT_Class_info类结构常量,继续查看name_index的索引值,十六进制为"0004",十进制为4,代表第四个常量,查看第四个常量,十六进制为"01",十进制为1,即tag为1的CONSTANT_Utf8_info结构,查看bytes内容为十六进制"0010",十进制为16,依次查看十六个字节为"6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74",内容是"java/lang/Object",第三项和第四项常量解析完成。
其他的常量,读者可以自行对照长量表进行解析,这里不再一一举例,jdk已经为我们提供了解析class文件的工具:Javap,使用javap命令可以查看class文件内容。
六、 访问标志
在常量池结束之后,紧接着的两个字节为访问标志(access_flags),这个标志用于标志类或者接口层次的访问信息,包括:这个class是类还是接口;类的访问权限;是否为抽象类;是否为final类型等。访问标识符总共有16个标志位可以使用,当前只定义了8个,还有8个没有使用到,具体的访问标志类型如下图所示:
标志名 | 标志值 | 标志含义 | 针对的对像 |
ACC_PUBLIC | 0x0001 | public类型 | 所有类型 |
ACC_FINAL | 0x0010 | final类型 | 类 |
ACC_SUPER | 0x0020 | 使用新的invokespecial语义 | 类和接口 |
ACC_INTERFACE | 0x0200 | 接口类型 | 接口 |
ACC_ABSTRACT | 0x0400 | 抽象类型 | 类和接口 |
ACC_SYNTHETIC | 0x1000 | 该类不由用户代码生成 | 所有类型 |
ACC_ANNOTATION | 0x2000 | 注解类型 | 注解 |
ACC_ENUM | 0x4000 | 枚举类型 | 枚举 |
标识符的表示与计算方式是多个访问标识符共用,然后做逻辑与计算,依然以之前的class文件为例,访问标志符为十六进制"0021",查找访问标识符可知,0021为0020和0001做逻辑与计算之后得到,即此类被ACC_SUPER和ACC_PUBLIC标识此类为公有访问权限,并且可以使用invokespecial字节码指令的新语义,如下图所示:
七、 类索引、父类索引及接口索引集合
类索引、父类索引都是一个u2类型的数据,接口索引则是一组u2类型索引的集合,class文件由着三类数据定义类和接口之间的集成及实现关系。类索引用于确定类的全限定名,父类索引用于确定父类的全限定名。各索引按照类索引、父类索引和接口索引的顺序排列,类索引和父类索引各自指向CONSTANT_Class_info类型。接口索引集合不同,接口索引第一项为接口索引计数器,用来标识共有多少项接口索引量,如果没有接口,则计数器为0。
继续以上述class文件为例,索引数据为十六进制"00 01 00 03 00 00",也就是类索引为"0001",十进制为1,第一个常量,内容是"com/tgb/lawyer/test/TestClass";父类索引为十六进制"00 03",十进制为3,第三项常量,内容为"java/lang/Object",即Object类,所有类的父类;接口索引为十六进制"00 00",十进制为0没有实现接口,如下图所示:
八、 字段表集合
字段表用来描述类或者接口中声明的变量,变量包括类变量和实例变量,但不包括方法中的局部变量。变量的可描述信息包括:名称;作用域(private、protected、public);实例变量还是类变量(static);可变性(final);并发可见性(volatile);是否可被序列化(transient);数据类型;初始化值。上述名称都可以使用布尔值或常量池标识,字段表结构如下图所示:
类型 | 名称 | 数量 |
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_VOLATILE | 0x0040 | 字段是否为volatile |
ACC_TRANSIENT | 0x0080 | 字段是否transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生 |
ACC_ENUM | 0x4000 | 字段是否为enum |
name_index为简单名称,引用了一个常量。简单名称和之前的全限定名的区别是没有包名,即只有类名。descriptor_index为一个u2类型的数据,是字段或者方法的描述符,字段表中是字段描述符,用来描述字段的数据类型和方法的参数列表及返回值类型。
描述符的数据类型包括byte、char、double、float、int、long、short、boolean以及代表方法返回值的void,数据类型使用一个大写字符来表示,对象烈性则用大写字符L加对象的全限定名来表示,如下图所示:
标识字符 | 含义 | 标识字符 | 含义 |
B | 基本类型byte | J | 基本类型long |
C | 基本类型char | S | 基本类型short |
D | 基本类型double | Z | 基本类型boolean |
F | 基本类型float | V | 特殊类型void |
I | 基本类型int | L | 对象类型,如Ljava/lang/Object |
继续以上述class文件为例,类索引之后为字段表集合,索引数据为十六进制"00 01 00 02 00 05 00 06 00 00"字段数量为一个u2类型,十六进制为"00 01",十进制为1,只有一个字段;紧接着为访问标志符,这两个字节为十六进制"00 02",查看字段访问标志符表格,"00 02"代表ACC_PRIVATE,私有变量;接下来是name_index,是一个u2类型,十六进制表示为"00 05",十进制为5,第五个常量,查看第五个常量,值为"x",接下来为descriptor_index,是一个u2类型,十六进制为"00 06",指向常量池第6个常量,值为"I",表示int类型数据;接下来为attributes_account,是一个u2类型数据,十六进制为"00 00",十进制为0,代表没有需要额外描述的信息。根据这些描述,我们可以推测这个字段为 private int x;。如下图所示:
九、 方法表集合
方法表集合跟字段表结构相同,access_flags表示访问标识符;name_index表示简单名称;descriptor_index表示方法描述符;attribute_info为其他描述符。方法集合和字段集合的区别是访问标识符跟其他描述符不同,方法表结构如下图所示:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法集合的访问标识符去掉了并发可见性(valatale)及序列化标志(transient),增加了线程安全标识符(syncronized)、本地方法标识符(native)、精确浮点描述符(strictfp)以及抽象方法描述符(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 | 字段是否由编译器自动产生 |
继续以上述class文件为例,字段表之后为方法表集合,十六进制数据为"00 02 00 01 00 07 00 08 00 01 00 09 00...",方法数量标识符为一个u2数据,十六进制为"00 02",十进制为2,有两个方法(虚拟机自动添加的构造方法及我们自己定义的方法)。紧接着一个u2类型数据表示方法描述符,十六进制为"00 01",标识为ACC_PUBLIC,是个公有方法;紧接着是简单名称,为一个u2类型数据"00 07",第7个常量,查找第7个常量,值为<init>;接下来是方法描述符即返回值类型,是一个u2类型数据,十六进制为"00 08",十进制为8,代表第8个常量,查询常量为V,代表无返回值类型void;接下来是附加方法描述符,数量为十六进制"00 01",即1个附加方法描述符,属性值为十六进制"00 09",代表第9个常量,查询常量可知,值为Code,说明此属性是方法的字节码描述,通过解析,可知第一个方法为public void <init>(){};,即类的构造函数。如下图所示:
后续方法不在解析,读者可以自行解析。
十、 属性表集合
属性表在之前的class文件、字段表和方法表均出现过,用于描述某些特定场景的信息。
属性表不在要求有严格的顺序、长度及内容信息,编译器可以自行实现这些内容,jdk1.7已经提供了21项属性,一个符合规则的数据表结构如下图所示:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u2 | attribute_length | 1 |
u1 | info | attribute_length |
十一 、 总结
class文件内容如上述所描述,我们的例子足够简略,真实的class文件构造一致,但是内容比较复杂,读者如果想要深入了解,还需要实际查看。