概述
Java字节码指令集是一组计算机指令,用于在Java虚拟机上执行Java程序。这些指令编码了操作码、操作数和控制信息,可以用于执行Java语言程序的所有操作,如变量赋值、方法调用、控制流与异常处理等。Java字节码指令集可以直接被Java虚拟机读取和解释,并且保证了Java程序在不同平台上的可移植性。
执行模型
如果不考虑异常处理的话,那么Java虚拟机的解释器可以使用下面这个伪代码当做最基本的执行模型来理解
1. do{ 2. 自动计算PC寄存器的值加1; 3. 根据PC寄存器的指示位置,从字节码流中取出操作码; 4. if(字节码存在操作数) 从字节码流中取出操作数; 5. 执行操作码所定义的操作; 6. }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类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据。与之类似,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型。
指令分析
由于完全介绍和学习这些指令需要花费大量时间。为了让大家能够更快地熟悉和了解这些基本指令,这里将JVM中的字节码指令集按用途大致分成9类。
- 加载与存储指令
- 算术指令
- 类型转换指令
- 对象的创建与访问指令
- 方法调用与返回指令
- 操作数栈管理指令
- 比较控制指令
- 异常处理指令
- 同步控制指令
(说在前面)在做值相关操作时:
- 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,可能是对象的引用)被压入操作数栈。
- 一个指令,也可以从操作数栈中取出一到多个值(pop多次),完成赋值、加减乘除、方法传参、系统调用等等操作。
加载与存储指令
作用
加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递。
常用指令
【局部变量压栈指令】将一个局部变量加载到操作数栈:
xload、xload_<n>
(其中x为i、l、f、d、a,n为0到3)【常量入栈指令】将一个常量加载到操作数栈:
bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>)、fconst_<f>、dconst_<d>
【出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:
xstore、xstore_<n>
(其中x为i、l、f、d、a,n为0到3);xastore
(其中x为i、l、f、d、a、b、c、s)扩充局部变量表的访问索引的指令:
wide
。
上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload_<n>)。这些指令助记符实际上代表了一组指令(例如iload_<n>代表了iload_0、iload_1、iload_2
和iload_3
这几个指令)。这几组指令都是某个带有一个操作数的通用指令(例如iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中。
除此之外,它们的语义与原生的通用指令完全一致(例如iload_0
的语义与操作数为0时的iload指令语义完全一致)。在尖括号之间的字母指定了指令隐含操作数的数据类型,<n>代表非负的整数,<i>代表是int类型数据,<l>代表long类型,<f>代表float类型,<d>代表double类型。
操作byte、char、short和boolean类型数据时,经常用int类型的指令来表示。
再谈操作数栈与局部变量表
操作数栈(Operand Stacks)
我们知道,Java字节码是Java虚拟机所使用的指令集。因此,它与Java虚拟机基于栈的计算模型是密不可分的。在解释执行过程中,每当为Java方法分配栈桢时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果。
具体来说便是:执行每一条指令之前,Java虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。
以加法指令iadd为例。假设在执行该指令前,栈顶的两个元素分别为int值1和int值2,那么iadd指令将弹出这两个int,并将求得的和int值3压入栈中。
由于iadd指令只消耗栈顶的两个元素,因此,对于离栈顶距离为2的元素,即图中的问号,iadd 指令并不关心它是否存在,更加不会对其进行修改。
局部变量表(Local Variables)
Java方法栈桢的另外一个重要组成部分则是局部变量区,字节码程序可以将计算的结果缓存在局部变量区之中。
实际上,Java虚拟机将局部变量区当成一个数组,依次存放this指针(仅非静态方法,即0位放的是this),所传入的参数,以及字节码中的局部变量。
和操作数栈一样,long类型以及double类型的值将占据两个单元,其余类型仅占据一个单元。
1. public void foo(long l, float f) { 2. { 3. int i = e; 4. } 5. { 6. String s = "Hello, World"; 7. } 8. }
this表示当前类的引用,l和f的类型的值占两个槽位,i和s变量由于分别在各自代码块中,没有共同的生命周期,所以占同一个槽位(即槽位复用)
在栈帧中,与性能调优关系最为密切的部分就是局部变量表。局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
局部变量压栈指令
指令 | 含义 |
iload |
从局部变量中装载int类型值 |
lload |
从局部变量中装载long类型值 |
fload |
从局部变量中装载float类型值 |
dload |
从局部变量中装载double类型值 |
aload |
从局部变量中装载引用类型值(refernce) |
iload_0 |
从局部变量0中装载int类型值 |
iload_1 |
从局部变量1中装载int类型值 |
iload_2 |
从局部变量2中装载int类型值 |
iload_3 |
从局部变量3中装载int类型值 |
lload_0 |
从局部变量0中装载long类型值 |
lload_1 | 从局部变量1中装载long类型值 |
lload_2 | 从局部变量2中装载long类型值 |
lload_3 | 从局部变量3中装载long类型值 |
fload_0 | 从局部变量0中装载float类型值 |
fload_1 | 从局部变量1中装载float类型值 |
fload_2 | 从局部变量2中装载float类型值 |
fload_3 | 从局部变量3中装载float类型值 |
dload_0 | 从局部变量0中装载double类型值 |
dload_1 | 从局部变量1中装载double类型值 |
dload_2 | 从局部变量2中装载double类型值 |
dload_3 | 从局部变量3中装载double类型值 |
aload_0 | 从局部变量0中装载引用类型值 |
aload_1 | 从局部变量1中装载引用类型值 |
aload_2 | 从局部变量2中装载引用类型值 |
aload_3 | 从局部变量3中装载引用类型值 |
iaload | 从数组中装载int类型值 |
laload | 从数组中装载long类型值 |
faload | 从数组中装载float类型值 |
daload | 从数组中装载double类型值 |
aaload | 从数组中装载引用类型值 |
baload | 从数组中装载byte类型或boolean类型值 |
caload | 从数组中装载char类型值 |
saload | 从数组中装载short类型值 |
局部变量压栈常用指令集
xload_n | xload_0 | xload_1 | xload_2 | xload_3 |
iload_n | iload_0 | iload_1 | iload_2 | iload_3 |
lload_n | lload_0 | lload_1 | lload_2 | lload_3 |
fload_n | fload_0 | fload_1 | fload_2 | fload_3 |
dload_n | dload_0 | dload_1 | dload_2 | dload_3 |
aload_n | aload_0 | aload_1 | aload_2 | aload_3 |
局部变量压栈指令剖析
局部变量压栈指令将给定的局部变量表中的数据压入操作数栈。
这类指令大体可以分为:
xload_<n>
(x为i、l、f、d、a,n为0到3)xload
(x为i、l、f、d、a
说明:在这里,x的取值表示数据类型。
指令xload_n表示将第n个局部变量压入操作数栈,比如iload_1、fload_0、aload_0等指令。其中aload_n表示将一个对象引用压栈。
指令xload通过指定参数的形式,把局部变量压入操作数栈,当使用这个命令时,表示局部变量的数量可能超过了4个,比如指令iload、fload等。
举个例子
1. public void load(int num, Object obj, long count, boolean flag, short[] arr) { 2. System.out.println(num); 3. System.out.println(obj); 4. System.out.println(count); 5. System.out.println(flag); 6. System.out.println(arr); 7. }
字节码执行过程:
常量入栈指令
指令 | 含义 |
aconst_null | 将null对象引用压入栈 |
iconst_m1 | 将int类型常量-1压入栈 |
iconst_0 | 将int类型常量0压入栈 |
iconst_1 | 将int类型常量1压入栈 |
iconst_2 | 将int类型常量2压入栈 |
iconst_3 | 将int类型常量3压入栈 |
iconst_4 | 将int类型常量4压入栈 |
iconst_5 | 将int类型常量5压入栈 |
lconst_0 | 将long类型常量0压入栈 |
lconst_1 | 将long类型常量1压入栈 |
fconst_0 | 将float类型常量0压入栈 |
fconst_1 | 将float类型常量1压入栈 |
dconst_0 | 将double类型常量0压入栈 |
dconst_1 | 将double类型常量1压入栈 |
bipush | 将一个8位带符号整数压入栈 |
sipush | 将16位带符号整数压入栈 |
ldc | 把常量池中的项压入栈 |
ldc_w | 把常量池中的项压入栈(使用宽索引) |
ldc2_w | 把常量池中long类型或者double类型的项压入栈(使用宽索引) |
常量入栈常用指令集
xconst_n | 范围 | xconst_null | xconst_m1 | xconst_0 | xconst_1 | xconst_2 | xconst_3 | xconst_4 | xconst_5 |
iconst_n | [-1, 5] | iconst_m1 | iconst_0 | iconst_1 | iconst_2 | iconst_3 | iconst_4 | iconst_5 | |
lconst_n | 0, 1 | lconst_0 | lconst_1 | ||||||
fconst_n | 0, 1, 2 | fconst_0 | fconst_1 | fconst_2 | |||||
dconst_n | 0, 1 | dconst_0 | dconst_1 | ||||||
aconst_n | null, String literal, Class literal | aconst_null | |||||||
bipush | 一个字节,2^8^,[-2^7^, 2^7^ - 1],即[-128, 127] | ||||||||
sipush | 两个字节,2^16^,[-2^15^, 2^15^ - 1],即[-32768, 32767] | ||||||||
ldc | 四个字节,2^32^,[-2^31^, 2^31^ - 1] | ||||||||
ldc_w | 宽索引 | ||||||||
ldc2_w | 宽索引,long或double |