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++;
    }
}

java 编译类文件路径 java编译文件_十六进制

五、   常量池

       紧接着主次版本号的内容为常量池,常量池在整个class文件中是篇幅最多的内容,也是与其他内容关联最多的数据类型。每个class文件的常量池都是不同的,所以常量池的入口需要使用一个常量池计数器(constant_pool_cont)来标志常量池中敞亮的数量。与其他集合类型数据(接口索引集合、字段集合、方法集合等)不同,常量池需要标志没有引用任何常量池,这里使用0来表示,所以常量池的数量表示是从1开始的,其他集合类型都是从0开始的,例如常量池数量22代表常量池中有21项常量。
       常量池中主要存放两种类型数据:字面量(Literal)和符号引用(Symbolic References),字面量及字符、常量等;符号引用包含三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。Java代码在进行编译的时候,没有"连接"这个步骤,所以在编译器不会确定各个常量的内存入口地址,因此常量池中会保存各个方法、字段的描述信息,当虚拟机运行时再从常量池中获取符号引用,所以常量池是跟其他内容关联最多的数据类型。
       常量池中每一项都是一个表,所有常量项名称均以"_info"结尾,我们以jdk1.7为例,jdk1.7共提供了14中表结构常量常量池结构如下图所示:

java 编译类文件路径 java编译文件_常量池_02


    

       对照常量表继续解析之前的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",第三项和第四项常量解析完成。

java 编译类文件路径 java编译文件_十六进制_03

       其他的常量,读者可以自行对照长量表进行解析,这里不再一一举例,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字节码指令的新语义,如下图所示:

java 编译类文件路径 java编译文件_常量池_04

七、   类索引、父类索引及接口索引集合

       类索引、父类索引都是一个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没有实现接口,如下图所示:

java 编译类文件路径 java编译文件_常量池_05


八、   字段表集合

       字段表用来描述类或者接口中声明的变量,变量包括类变量和实例变量,但不包括方法中的局部变量。变量的可描述信息包括:名称;作用域(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;。如下图所示:

java 编译类文件路径 java编译文件_常量池_06

九、   方法表集合

        方法表集合跟字段表结构相同,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>(){};,即类的构造函数。如下图所示:

java 编译类文件路径 java编译文件_常量池_07

后续方法不在解析,读者可以自行解析。

十、   属性表集合

       属性表在之前的class文件、字段表和方法表均出现过,用于描述某些特定场景的信息。
       属性表不在要求有严格的顺序、长度及内容信息,编译器可以自行实现这些内容,jdk1.7已经提供了21项属性,一个符合规则的数据表结构如下图所示:

类型

名称

数量

u2

attribute_name_index

1

u2

attribute_length

1

u1

info

attribute_length

十一 、  总结

 class文件内容如上述所描述,我们的例子足够简略,真实的class文件构造一致,但是内容比较复杂,读者如果想要深入了解,还需要实际查看。