第六章 类文件结构

6.1 概述

      代码编译的结果是从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。由于最近10年内虚拟机及建立在虚拟机之上的大量程序设计语言如雨后春笋般出现并蓬勃发展,将我们的程序编译成二进制本地机器码已不再是唯一的选择,越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。

6.2 无关性的基石

      各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码是构成平台无关性的基石。使用java编译器可以吧java代码编译为存储字节码的class文件,使用JRuby等其他语言的编译器一样可以把程序代码编译成class文件,虚拟机并不关心class的来源是什么语言,只要它符合class文件应有的结构就可以在java虚拟机中运行。

      java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比java语言本身更强大。因此有一些java语言本身无法有效支持的语言特性并不代表字节码本身无法有效支持,这也为其他语言实现一些有别于java的语言特性提供了基础。

6.3 Class类文件的结构

      Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全是程序运行的必要数据,没有空隙存在。当遇到有8位字节以上空间的数据项时,则会按照高位在前的方式 分割成若干个8位字节进行存储。

      根据java虚拟机规范的规定,Class文件格式采用一种类似于c语言结构体的伪结构来存储,这种伪结构只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础。

      无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串。表是由多个无符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性的以“info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。

6.3.1 魔数与Class文件的版本

      每个Class文件的头4个字节称为魔数,它的唯一作用是用于确定这个文件是否为一个能被虚拟机接受的class文件。使用魔数而不使用扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以很随意的被改动。

      紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号,第7和第8个字节是主版本号。java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加一,高版本的JDK可以兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并没有发生改变。

6.3.2 常量池

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

      由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。与java中的语言习惯不同,这个容量计数是从1而不是0开始的,常量池容量(偏移地址:0x00000008)为16进制数0x0016,即十进数的22,这就代表常量池中有21项常量,索引值为1-21.制定Class文件规范时,将第0项常量空出来是有特殊考虑的,这样做是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的意思,这种情况就可以把索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始的,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。

      常量池之中主要存放两大类常量:字面量和符号引用。字面量比较接近于java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。

6.3.3 访问标志

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

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

      类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的java类都有父类,因此除了java.lang.Object之外,所有java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口的索引集合中。

6.3.5 字段表集合

      字段表用于描述接口或类中声明的变量。字段包括了类级变量或实例级变量,但不包括在方法内部声明的变量。

6.3.6 方法表集合

      方法表的结构如字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。方法的定义可以通过访问标志、名称索引、描述符索引表达清楚方法里的代码去哪里了?方法里的java代码经过编译器编译成字节码之后,存放在方法属性表集合中一个名为“code”属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目。

6.3.7 属性表集合

      属性表在前面的讲解之中已经出现过数次,在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

a、code属性:java程序方法体里面的代码经过javac编译器处理之后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或抽象类中的方法就不存在Code属性。

b、Exceptions属性:作用是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。

c、LineNumberTable属性:用于描述java源码行号与字节码行号之间的对应关系。

此外还有SourceFile属性、ConstantValue属性、InnerClasses属性、Deprecated及Synthetic属性等等。

猜你喜欢

转载自blog.csdn.net/strawqqhat/article/details/83833423