Java编译原理--类文件结构

       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文件构造一致,但是内容比较复杂,读者如果想要深入了解,还需要实际查看。
    

猜你喜欢

转载自blog.csdn.net/u010942465/article/details/81172667