1 无关性的基石
- 平台无关性的基石:各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)
- 语言无关性的基石:虚拟机和字节码存储格式。
2 Class 类文件的结构
- Class 文件是一组以8位字节为基础单位的二进制流;8位字节以上以高位在前分割
- 只有两种数据类型:无符号数和表;
- 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串;
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型
- 整个 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 | field_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
2.1 魔数与 Class 文件的版本
- 每个 Class 文件的头 4 个字节称为魔数,唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。
- 紧接着魔数的 4 个字节存储的是 Class 文件的版本号:第 5 和第 6 个字节是次号,第 7 和第 8 个字节是主版本号。
2.2 常量池
- 常量池的入口需要放置一项u2类型的数据,代表常量池容量计数,从1开始。
- 常量池中主要存放两大类常量:字面量和符号引用
- 字面量,比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等
- 符号引用,则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中
2.3 访问标志
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为 public 类型 |
ACC_FINAL | 0x0010 | 是否被声明为 final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令的新语意 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
2.4 类索引、父类索引与接口索引集合
- 类索引(this_class)是一个 u2 类型的数据,用于确定这个类的全限定名
- 父类索引(super_class)是一个 u2 类型的数据,父类索引用于确定这个类的父类的全限定名
- 接口索引集合(interfaces)是一组 u2 类型的数据的集合
- Class 文件中由这三项数据来确定这个类的继承关系
2.5 字段表集合
- 字段表(field_info)用于描述接口或者类中声明的变量
- 字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量
字段表的结构如下表:
名称 | 类型 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
- name_index 是对常量池的引用,代表着字段的简单名称
- descriptor_index 是对常量池的引用,代表字段的描述符
- 全限定名:“org/fenixsoft/clazz/TestClass”是这个类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束
- 简单名称:指没有类型和参数修饰的方法或者字段名称,inc()方法和m字段的简单名称分别是“inc”和“m”。
- 描述符:作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
描述符
标识字符 | 含义 |
---|---|
B | 基本类型 byte |
C | 基本类型 char |
D | 基本类型 double |
F | 基本类型 float |
I | 基本类型 int |
J | 基本类型 long |
S | 基本类型 short |
Z | 基本类型 boolean |
V | 特殊类型 void |
L | 对象类型,如 Ljava/lang/Object |
- 数组描述符,每一维度将使用一个前置的“[”字符来描述
- 如一个定义- 为“java.lang.String[][]”类型的二维数组,将被记录为:“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录为“[I”
- 方法描述符,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内
- void inc()的描述符为 “()V”,
- java.lang.String toString() 的描述符为 “()Ljava/lang/String;”,
- int indexOf(char[]source, int sourceOffset, int sourceCount,char[] target, int targetOffset, int targetCount, int fromIndex)的描述符为 “([CII[CIII)I”
2.6 方法表集合
- 方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项
- 方法重载要求具备不同特征签名,返回值并不包含在方法的特征签名中,所以返回值不参与重载选择,但在 Class 文件中,只要描述符不完全一致的两个方法就可以共存。也就是说两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个 Class 文件中的。
2.7 属性表集合
- 属性表(attribute_info)在前面的讲解之中已经出现过数次,在 Class 文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
- Code 属性 Java 程序方法体中的代码经过 Javac 编译器处理后,最终变为字节码指令存储在 Code 属性内。Code 属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在 Code 属性
3 字节码指令简介
Java 虚拟机的指令由一个字节长度的、代表着特定操作含义的数字(操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成。
由于 Java 虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。
在指令集中大多数的指令都包含了其操作所对应的数据类型信息
- 加载和存储指令
- iload/iload 等(加载局部变量到操作栈)
- istore/istore 等(从操作数栈存储到局部变量表)
- bipush/sipush/ldc/iconst_(加载常量到操作数栈)
- wide(扩充局部变量表访问索引)
- 运算指令
- 没有直接支持 byte、short、char 和 boolean 类型的算术指令而采用 int 代替
- iadd/isub/imul/idiv 加减乘除
- irem 求余
- ineg 取反
- ishl/ishr 位移
- ior 按位或
- iand 按位与
- ixor 按位异或
- iinc 局部变量自增
- dcmpg/dcmpl 比较
- 类型转换指令
- i2b/i2c/i2s/l2i/f2i/f2l/d2i/d2l/d2f;
- 对象创建与访问指令
- new 创建类实例
- newarray/anewarray/multianewarray 创建数组
- getfield/putfield/getstatic/putstatic 访问类字段或实例字段
- baload/iaload/aaload 把一个数组元素加载到操作数栈
- bastore/iastore/aastore 将一个操作数栈的值存储到数组元素中
- arraylength 取数组长度、instanceof/checkcast 检查类实例类型
- 操作数栈管理指令
- pop/pop2 一个或两个元素出栈
- dup/dup2 复制栈顶一个或两个数组并将复制值或双份复制值重新压力栈顶
- swap 交互栈顶两个数值;
- 控制转移指令
- ifeq/iflt/ifnull 条件分支
- tableswitch/lookupswitch 复合条件分支
- goto/jsr/ret 无条件分支
- 方法调用和返回指令
- invokevirtual/invokeinterface/invokespecial/invokestatic/invokedynamic 方法调用
- ireturn/lreturn/areturn/return 方法返回
- 异常处理指令
- athrow
- 同步指令
- monitorenter/monitorexit