无关性的基石
计算机底层只能识别二进制,由CPU直接处理二进制,在底层上面是操作系统,在操作系统上面就是虚拟机,java有一个口号,"一次编写,到处运行"这个不太可能在操作系统层面上实现,不同的操作系统肯定将会长期并存发展,所以这个理想只能在"应用层"实现。
在虚拟机中使用字节码,而所有的指令都会转成字节码,传递给虚拟机
Class类文件的结构
class文件是一串严格有序
、无分隔符
、无对齐符
的二进制流
。
class文件伪结构如下
Java
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
魔数与 Class 文件的版本
每个Class文件的头四个字节称为魔数,他的作用是确定这个文件是否可以被虚拟机接受的Class文件
魔数的 4 个字节存储的是 Class 文件的版本号:第5和第 6个字节是次版本号,第 7 和第 8 个字节是主版本号
高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件
Class 文件格式采用一种类似于 C 语言结构体的伪结构来存 储数据,这种伪结构中只有两种数据类型:"无符号数"和"表"
- 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节 和 8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型
常量池
常量池可以比喻为 Class 文件里的资源仓库
在常量池的入口需要放置 一项 u2类型的数据,代表常量池容量计数值,这个从0开始不是1开始
常量池中主要存放两大类常量:字面量和符号引用
字面量就是java中的常量概念
符号引用属于编译原理方面的概念,主要包括下面的常量:
被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
在 Class 文件中不会保存各个方法、字段最终在内存中的 布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是 无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
常量池中每一项常量都是一个表,他有17个类表,表示17种不同类型的常量
17 类表都有一个共同的特点,表结构起始的第一位是个 u1 类型的标志位,代表着当前常量属于哪种常量类型。
这17个表每一个表的结构都不一样各自有着完全独立的数据结构,首位都是tag来标记数据类型,后面的根据不同的需要设计不同
访问标志
在常量池结束之后,紧接着的 2 个字节代表访问标志
这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final;等等
access_flags 中一共有 16 个标志位可以使用,当前只定义了其中 9 个,没有使用到的标志位要求一律为零
类索引、父类索引与接口索引集合
类索引和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定该类型的继承关系
接口索引集合,入口的第一项 u2 类型的数据为接口计数器,表示索引表的容量。
字段表集合
描述接口或者类中声明的变量
字段中可以包含各种各样的修饰符,这些修饰符可以使用标志位处理,但是字段叫什么名字,什么类型是无法确定的,只能使用常量池中的常量来描述
在Class文件中字段表集合的第一个U2类型数据为容量计数器,即记录字段表集合数量,之后就是上面的表中内容
方法表集合
方法表的结构如同字段表一样,依次包括访问标志 、名称索引、描述符索引 、属性表集合几项
方法的定义通过过访问标志、名称索引、描述符索引来表达清楚,方法中的代码内容会经过Javac 编译器编译成字节码指令之后,存放在方法属性表集合中一个名为"Code"的属性里面
方法表集合和字段表集合一样在具体描述表之前会有一个u2类型数据描述方法表集合的大小,代表这个集合中有几个方法
属性表集合
属性表在前面的讲解之中已经出现过数次,Class 文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。
只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。
对于每一个属性,它的名称都要从常量池中引用一个 CONSTANT_Utf8_info 类型的 常量来表示,而属性值的结构则是完全自定义的,只需要通过一个 u4 的长度属性去说 明属性值所占用的位数即可
总结
还是这张图
一个Class文件首先是四个字节的一个魔数用来表示是否可以被虚拟机接收,然后是版本号,次级版本号,然后是常量池中存储我们需要使用的数据,不光是编译前的数据还包括编译时的数据比如接口限定名等,在这里首先会使用一个u2的大小记录常量池大小然后在根据不同记录常量,在常量记录完毕之后是访问标识是一些类的访问信息,然后是类索引父类索引等,接下来是接口索引记录接口数量,然后会有一个接口池记录接口信息,接口记录完毕后面就是字段表,方法表,属性表的记录,这些表都有一个计数器记录大小
- 魔数
- 版本,次版本
- 访问标志
- 常量池
- 类,父类,接口索引,
- 字段表,方法表,属性表
字节码指令简介
Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字 (称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需的参数(称为操作数, Operand)构成。
指令集因为他的操作码只有一个数字所以他的操作码指令数不能超过256条,这样它在处理一些超过一个字节的数据时需要从字节中重建出具体的数据结构,他会损失掉一些性能,好处在于放弃了操作数长度对齐,省略掉大量的填充和间隔符号
字节码和数据类型
在java的指令集中,大部分指令都有包含其操作对应的数据类型信息,
对于大部分与数据类型相关的字节码指令,它们的 **操作码助记符
**中都有特殊的字符来表明专门为哪种数据类型服务:
- i 代表对 int 类型的数据操作,
- l 代表 long,s 代表 short,
- b 代表 byte,c 代表 char,
- f 代表 float,d 代表 double,
- a 代表 reference。
也有一些指令的助记符没有明确指明操作类型的字母
因为操作集只有一个字节所以它没有设计所有的数据类型的操作码,Java 虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它,在处理的时候如果发现某一个数据类型没有对应的指令集会对其进行相关的扩展比如编译器会在编译期或运行期将 byte 和 short 类型的 数据带符号扩展为相应的 int 类型数据
加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括:
- 将一个局部变量加载到操作栈:iload、iload_、lload、lload_、fload、 fload_、dload、 dload_、aload、aload_
- 将一个数值从操作数栈存储到局部变量表:istore、istore_、lstore、 lstore_、fstore、 fstore_、dstore、dstore_、astore、astore_
- 将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、 iconst_m1、 iconst_、lconst_、fconst_、dconst_
- 扩充局部变量表的访问索引的指令:wide
指令助记符中,有一部分是以尖括号结尾的(例如 iload_),这些 指令助记符实际上代表了一组指令(例如 iload_,它代表了 iload_0、iload_1、 iload_2 和 iload_3 这几条指令)。这几组指令都是某个带有一个操作数的通用指令(例如 iload)的特殊形式,对于这几组特殊指令,它们省略掉了显式的操作数,不需要进行取 操作数的动作,因为实际上操作数就隐含在指令中。除了这点不同以外,它们的语义与 原生的通用指令是完全一致的