这一节主要讲述类文件(.class)的文件结构。

1.无关性:

商业机构和开源机构已经在Java语言之外发展处一大批在Java虚拟机之上运行的语言,如Clojure、Groovy、JRuby、Jython、Scala等。使用这些语言编译器一样可以把程序代码编译成Class文件,虚拟机并不关心Class文件的来源是何种语言。

2.Class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上的空间的数据项时,则会按照高位在前(这种顺序称为“Big-Endian”,具体是指最高位字节在地址最低位、最低位字节在地址最高位的顺序来存储数据)的方式分割成若干个8位字节进行存储。

根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构存储数据,这种伪结构体中只有两种数据类型:无符号数和表。

无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性地以_info结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。

Class的结构不像XML等描述语言。由于它没有任何分隔符号,所以无论是顺序还是数量,甚至是数据存储的字节序这样的细节,都是被严格限定的,哪个字节代表了什么含义,长度是多少,先后顺序如何,都不允许改变。接下来我们将一起看看表中各个数据项的具体含义。

在这之前我们首先编译一段代码:

将编译后的Class文件使用WinHex这类十六进制文本查看器进行查看。

可以看到十六进制编码,下面就一起开分析这些编码都描述了一些什么事情。

2.1 魔数与Class文件的版本

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用了魔数来进行身份识别,譬如图片格式,如gif或者jpeg等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。Class文件的魔数的获得很有浪漫气息,值为“0xCAFEABE(咖啡宝贝)”,这个魔数值在Java还称作“Oak”语言的时候就已经确认下来了。

紧接着魔数的四个字节存储的是Class文件的版本号。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

不同JDK版本对应的版本号如下:

JDK 1.8 = 52 
JDK 1.7 = 51 
JDK 1.6 =50 
JDK 1.5 = 49 
JDK 1.4 = 48 
JDK 1.3 = 47 
JDK 1.2 = 46 
JDK 1.1 = 45

2.2 常量池

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中出现的第一个表类型数据项目。

由于常量池的大小是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值,并且这个值与java的0开始不一样,这个值从1开始,也就是说如果大小为0x13,这样常量池就有编号从1~18个常量。

常量池中主要存放两大类常量:字面量和符号引用。

字面量比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等。

符号引用则属于编译原理方面的概念,包括了下面三类常量:

类和接口的全限定名;

字段的名称和描述符;

方法的名称和描述符。

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态链接。也就是说,在Class文件中不会保存各个方法、字段的最终内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应符号的引用,再在类创建时或运行时解析、翻译到具体的内存地址中。

在JDK1.7之前有14中常量项类型,他们每一种类型的具体表结构有不一样,具体如下:

在翻译之前,我们可以先使用JDK自带的工具在命令行中查看常量池。

javap -verbose FILE_NAME

可以看到确实有编号为1~18的常量。并且在每一种常量编号后都有他们的常量类型(Methodref、Fieldref、Class、Utf8)

根据这个表我们就可以翻译之前的字节码:

2.3 访问标志

在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。

本例中,只有ACC_PUBLIC和ACC_SUPER为真,其余为假,因此access_flags的值为0x0001|0x0020=0x0021。

2.4 类索引、父类索引与接口索引集合

类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。

类索引用于确定这个类的全限定名。

父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java都有父类,因此除了该类外,所有的Java类的父类索引都不为0。

接口索引集合就是用来描述这个类实现了哪些接口,这些被实现的接口将按照implements语句后的接口顺序从左到右排列在接口索引集合中。

因为该类并没有实现接口,因此接口索引集合就是00 00

对比下表,可以看出该类为a类,父类为Object、没有实现接口。

2.5 字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。

我们可以想一想在Java中描述一个字段可以包含什么信息?

可以包含有:字段的作作用域(public、private、protected)、是实例变量还是类变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本数据类型、对象、数组)、字段名称。

上述这些信息中,各个修饰符都是布尔值,要么有要么没有,很适合用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型,这些都无法固定,只能引用常量池中的常量来描述。

全限定名:xxx/xxxx/xxxxx

简单名:xxxxx

十六进制机器码:

从这几个数据中,我们就可以看出,字段表长度为1,为私有变量,名字为m,类型为int。于是我们可以看出private int m;

最后面还有一个用来存储额外属性的计数器,本例为0。

2.6 方法表集合

方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目。

第一个u2类型的数据为计数器容量,值为0x0002,代表集合中有两个方法(这两个方法为编译器添加的实例构造器<init>和源码中的方法inc())。第一个方法的访问标志值为0x0001,也就是只有ACC_PUBLIC为真;名称索引为0x0007,对应为<init>,然后是0x0008,为描述符索引,查常量池表可知为()V,然后是属性计数器为1,说明后面有一个属性。然后是属性名索引为Code,说明此属性是方法的字节码描述。

与字段表集合相对应的,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的就是类构造器<init>方法和实例构造器<init>方法。

在Java语言中,要重载一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不用的特征签名(就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的)。但是在Class文件格式中,特征签名的范围会大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于一个Class文件中的。

2.7 属性表集合

2.7.1 Code属性

Java程序经过Javac编译器处理后,最终变成字节码指令存储在Code属性内。

Code属性结构如下:

max_stack代表了操作数栈深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机在运行的时候需要根据这个值来分配栈帧中的操作栈深度。

max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long则占用两个Slot来存放。

方法参数(包括实例方法中的this)、显示异常处理的参数(try-catch中的异常)、方法体中定义的局部变量都需要使用局部变量来存放。另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占的Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超过一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所重用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。

code_lengthcode用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code用来存储字节码指令的一系列字节流。一个u1代表一条指令,也就是说最多有256种指令,目前java虚拟机规范已经定义了其中约200条编码值对应的虚拟机指令。

关于code_length,虽然是一个u4类型字节,也就是65535条字节码指令,但是它实际上只是用了u2的长度,如果超过了这个限度,虚拟机也会拒绝执行。

2.7.2 Exceptions属性

2.7.3 LineNumberTable属性

2.7.4 LocalVariableTable属性

2.7.5 SourceFile属性

2.7.6 ConstantValue属性

2.7.7 InnerClasses属性

2.7.8 Deprecated和Synthetic属性

2.7.9 StackMapTable属性

2.7.10 Signature属性

2.7.11 BootstrapMethods属性


0 条评论

发表回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用 * 标注