这里写目录标题
第6章 类文件结构
代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。
任何一门程序 语言能够获得商业上的成功,都不可能去做升级版本后,旧版本编译的产品就不再能够运行这种事 情。
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文 件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数
据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前[2]的方式分割 成若干个8个字节进行存储。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数 据,这种伪结构中只有两种数据类型:“无符号数”和“表”
- 无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串 值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名 都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视 作是一张表,这张表由表6-1所示的数据项按严格顺序排列构成。
类文件的结构
- 前四个字节是魔数:唯一作用是确定这个文件是否为 一个能被虚拟机接受的Class文件。不仅是Class文件,很多文件格式标准中都有使用魔数来进行身份识别的习惯
- 四个字节的版本号。高版本的JDK能 向下兼容以前版本的Class文件,但不能运行以后版本的Class文件
- 常量池:常量池可以比喻为Class文件里的资源仓库,它是Class 文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它 还是在Class文件中第一个出现的表类型数据项目。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比 较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译 原理方面的概念,主要包括下面几类常量:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic) ·动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
UTF-8缩略编码与普通UTF-8编码的区别是: 从’\u0001’到’\u007f’之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示, 从’\u0080’到’\u07ff’之间的所有字符的缩略编码用两个字节表示,从’\u0800’开始到’\uffff’之间的所有字符 的缩略编码就按照普通UTF-8编码规则使用三个字节表示。
访问标志
在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或 者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract 类型;如果是类的话,是否被声明为final;等等。具体的标志位以及标志的含义见表6-7。
1个字节(8位bit)=2个16进制字符,一个16进制位=0.5个字节。
类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合
(int erfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。
类索 引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多 重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接 口,这些被实现的接口将按 implements
关键字(如果这个Class文件表示的是一个接口,则应当是 extends 关 键 字 ) 后 的 接 口 顺 序 从 左 到 右 排 列 在 接 口 索 引 集 合 中 。
字段表集合
字段表(field_info)用于描述接口或者类中声明的变量,逻辑上只有是或者不是的,以后都可以考虑使用bit位来标识。
方法表集合
属性表集合
属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以
携带自己的属性表集合,以描述某些场景专有的信息。
1.Code 属性
Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。 Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽 象类中的方法就不存在Code属性,如果方法表有Code属性存在,那么它的结构将如表6-15所示。
- max_stack代表了操作数栈帧深度的最大值。
- max_locals代表了局部变量表所需的存储空间
- code_length 和 code 用 来 存 储 Java 源 程 序 编 译 后 生 成 的 字 节 码 指 令 。 code_length 代 表 字 节 码 长 度 , code是用于存储字节码指令的一系列字节流。既然叫字节码指令,那顾名思义每个指令就是一个u1类 型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指 令,并且可以知道这条指令后面是否需要跟随参数,以及后续的参数应当如何解析。我们知道一个u1 数据类型的取值范围为 0x000xFF,对应十进制的0255,也就是一共可以表达256条指令
在任何实例方法里面,都可以通过“this”关键字访问到此方 法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现非常简单,仅仅是通过在Javac编 译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法 时自动传入此参数而已
2.Exceptions 属 性
Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也 就是方法描述时在throws关键字后面列举的异常
3. LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系
4.LocalVariableTable 及 LocalVariableTypeTable属性
以及等等各个方面的属性
14.运行时注解相关属性
早在JDK 5时期,Java语言的语法进行了多项增强,其中之一是提供了对注解(Annotation)的支 持 。 为 了 存 储 源 码 中 注 解 信 息 , C l a s s 文 件 同 步 增 加 了 R u n t i m e Vi s i b l e A n n o t a t i o n s 、
R u n t i m e I n v i s i b l e A n n o t a t i o n s 、 R u n t i m e Vi s i b l e P a r a m e t e r A n n o t a t i o n s 和 R u n t i m e I n v i s i b l e P a r a m e t e r - Annotations四个属性。到了JDK 8时期,进一步加强了Java语言的注解使用范围,又新增类型注解 (JSR 308),所以Class文件中也同步增加了RuntimeVisibleTypeAnnotations和 RuntimeInvisibleTypeAnnotations两个属性。
字节码指令简介
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Op code) 以及跟随其后的零至多个代表此操作所需的参数(称为操作数,Op erand)构成。由于Java虚拟机采用 面向操作数栈而不是面向寄存器的架构(这两种架构的执行过程、区别和影响将在第8章中探讨),所 以大多数指令都不包含操作数,只有一个操作码,指令参数都存放在操作数栈中。
- 字节码指令流基本上都是单字节对齐的,只有“tableswitch”和“lookupswitch”两条指令例外,由于它 们的操作数比较特殊,是以4字节为界划分开的,所以这两条指令也需要预留出相应的空位填充来实现 对齐。