概述
- Java字节码对于虚拟机,好像汇编语言对于计算机,属于基本执行指令
- Java虚拟机的指令由一个字节长度的,代表某种特定操作含义 的数字(称为操作码Opcode)以及跟随其后的零至多个代表此操作所需参数(操作数,Operands)而构成,由于Java虚拟机采用面向操作数栈而不是寄存器的结构,大多数指令都不包含操作数,只有一个操作码
- 由于限制了Java虚拟机操作码的长度为一个字节(0-255),意味着指令集的操作码总数不可能超过256条
- 熟悉虚拟机的指令对于动态字节码生成、反编译Class文件、Class文件修补都有着非常重要的价值。
执行模型
- 如不考虑异常处理的话,那么Java虚拟机的解释器可以使用下面这个伪代码当做最基本的执行模型
do {
自动计算PC寄存器的值加1;
根据PC寄存器的指示位置,从字节码流中取出操作码;
if(字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
} while(字节码长度>0);
字节码与数据类型
- 在Java指令集中,大多数的指令都包含了其操作所对应的数据类型停止,如iload指令用于从局部变量表中加载int型的数据到操作数栈,而fload指令加载的则是float类型的数据
- 对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务
- i代表int类型
- l代表long
- s代表short
- b代表byte
- c代表char
- f代表float
- d代表double
- 也有一些指令的助记符中没有明确地指明操作类型的字母,如arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数据类型的对象
- 无条件跳转指令goto则是与数据类型无关的
- 大部分的指令都没有支持整数类型byte,char和short,甚至没有任何指令支持boolean类型,编译器会在编译期或运行期将byte和short类型的数据带符号扩展为相应的int类型数据,将boolean和char类型数据零位扩展为相应的int类型数据,与之类似,在处理byte,char,boolean和short类型的数组时,也会转换为使用对应int类型的字节码指令来处理,
指令分类
- 加载与存储指令
- 算术指令
- 类型转换指令
- 对象创建与返回指令
- 方法调用与返回指令
- 操作数栈管理指令
- 比较控制指令
- 异常处理指令
- 同步控制指令
- 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据被压入操作数栈
- 一个指令,也可以从操作数栈中取出一到多个值(pop多次),完成赋值,加减乘除、方法传参、系统调用 等等操作
加载与存储指令
- 作用:加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递
- 常用指令
- 再谈操作数栈与局部变量表
- 局部变量压栈指令:将一个局部变量加载到操作数栈:xload、xload(x为i,l,f,d,a,n为0-3);xaload,xaload_(其中x为 i,l,f,d,a,b,c,s,n为0-3)
- 常量入栈指令:将一个常量加载到操作数栈:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_m1,iconst_,lconst_,fconst_,dconst_
- 出栈装入局部变量表指令:将一个数值从操作数栈存储到局部变量表:xstore,xstore_(其中x为i,l,f,d,a,n为0-3);xastore(其中x为 i,l,f,d,a,b,c,s)
- 扩展局部变量表的访问索引的指令:wide
- 上面的指令助记符中,有一部分是以尖括号结尾的(iload_),这些助记符实际代表了一组指令(iload_代表iload_0,iload_1,iload_2,iload_3这几个指令),这几组指令都是带有一个操作数的通用指令的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数隐含在指令中
- 除此之外,它们的语义与原生的通用指令完全一致(如iload_0的语义与操作数为0时的iload指令语义完全一致),在尖括号之间的字母指定了指令隐含操作数的数据类型,代表非负整数,代表是int类型数据,代表long类型,代表float,代表double类型
再谈操作数栈
- Java字节码是Java虚拟机所使用的指令集,它与Java虚拟机基于栈的计算模型是密不可分的,在解释执行过程中,每当Java方法分配栈桢时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数及返回结果
- 具体来说,执行每一条指令之前,Java虚拟机要求该指令的操作数已经被压入操作数栈中,在执行指令时,Java虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中

- 以加法指令iadd为例,假设在执行该指令前,栈顶的两个元素分别为int值1和int值2,那么iadd指令将弹出两个int,并将求得的和int值3压入栈中

- 由于iadd指令只消耗栈顶的两个元素,因此,对于离栈顶距离为2的元素,即图中问号,iadd指令并不关心它是否存在,更加不会对其进行修改
- Java方法栈帧的另一个重要组成部分是局部变量区,字节码程序可以将计算的结果缓存在局部变量区中
- Java虚拟机将局部变量区当成一个数组,依次存放this指针(仅非静态方法),所传入的参数,以及字节码中的局部变量
- 和操作数栈一样,long类型及double类型的值占用两个单元,其余类型占据一个单元
public void foo(long l,float f) {
{
int i = 0;
}
{
String s = "Hello,World";
}
}

- 在栈帧中,与性能调优关系最为密切的部分就是局部变量表,局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
- 在方法执行时,虚拟机使用局部变量表完成方法传递
局部变量压栈指令

常量入栈指令