入栈指令
iconst_<i> :
i范围:[-1, 5]
将int类型(byte, short,char, boolean)常量i压入操作数栈
i为-1时iconst_m1
lconst_<l>:
l范围:[0, 1]
将long类型常量l压入操作数栈
fconst_<f>:
f范围:[0, 2]
将float类型常量f压入操作数栈
dconst_<d>:
d范围:[0, 1]
将double类型常量d压入操作数栈
dconst_<d>:
d范围:[0, 1]
将double类型常量d压入操作数栈
aconst_null
将引用类型null压入操作数栈
bipush:接收8位整数作为参数,将其压入操作数栈
sipush:接收16位整数作为参数,将其压入操作数栈
ldc:上方指令不能满足的需求用ldc实现。可以收8位的参数, 该参数指向常量池的int,float或者String的索引,将其压入操作数栈
ldc_w接收两个8位参数
压入栈是double或long用ldc2_w
类型 | 常数指令 | 范围 |
---|---|---|
int ( byte, short, char, boolean) | iconst | [-1, 5] |
int ( byte, short, char, boolean) | bipush | [-128, 127] |
int ( byte, short, char, boolean) | sipush | [-32768, 32767] |
int ( byte, short, char, boolean) | ldc | any int value |
long | lconst | 0, 1 |
long | ldc | any long value |
float | fconst | 0, 1, 2 |
float | ldc | any float value |
double | dconst | 0, 1 |
double | ldc | any double value |
reference | aconst_null | null |
reference | ldc | 索引 |
public void pushConstLdc() {
int i = -1;
int a = 5;
int b = 6;
int c = 127;
int d = 128;
int e = 32767;
int f = 32768;
}
0: iconst_m1 ----->-1入栈
1: istore_1 ----->-1存放到局部变量表1号槽
2: iconst_5 ----->5入栈
3: istore_2 ----->5存放到局部变量表2号槽
4: bipush 6 ----->6入栈
6: istore_3 ----->6存放到局部变量表3号槽
7: bipush 127 ----->127入栈
9: istore 4 ----->127存放到局部变量表4号槽
11: sipush 128 ----->128入栈
14: istore 5 ----->128存放到局部变量表5号槽
16: sipush 32767 ----->32767入栈
19: istore 6 ----->32767存放到局部变量表6号槽
21: ldc #2 // int 32768 ----->32768入栈
23: istore 7 ----->32768存放到局部变量表7号槽
25: return
public void constLdc() {
long a1 = 1;
long a2 = 2;
float b1 = 2;
float b2 = 3;
double c1 = 1;
double c2 = 2;
Data d = null;
}
0: lconst_1 ----->-1入栈
1: lstore_1 ----->1存放到局部变量表1号槽
2: ldc2_w #2 // long 2l ----->2入栈
5: lstore_3 ----->2存放到局部变量表3号槽(long类型占两个槽位)
6: fconst_2 ----->2入栈
7: fstore 5 ----->2存放到局部变量表5号槽(long类型占两个槽位)
9: ldc #4 // float 3.0f ----->3入栈
11: fstore 6 ----->2存放到局部变量表6号槽(float类型占一个槽位)
13: dconst_1 ----->1入栈
14: dstore 7 ----->1存放到局部变量表7号槽(float类型占一个槽位)
16: ldc2_w #5 // double 2.0d ----->2入栈
19: dstore 9 ----->2存放到局部变量表9号槽(double类型占一个槽位)
21: aconst_null ----->null入栈
22: astore 11 ----->null存放到局部变量表11号槽(double类型占一个槽位)
24: return
比较指令
- 比较指令的作用是比较栈顶两个元素的大小,并将比较结果入栈
- 比较指令有:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
dcmpg,dcmpl为double类型
fcmpg、fcmpl为float类型
lcmp为long类型 - 对于double和float类型的数字,由于存在NaN类型,各有两个版本的比较指令。long型整数无NaN故只需要一套。
说明:
指令fcmpg和fcmpl都从栈中弹出两个操作数,并将它们作比较,设栈顶元素为v2,栈顶顺位第2位元素为v1,若v1=v2则压入0;若v1>v2则压入1, 若v1<v2则压入-1.不同之处为如果遇到NaN,fcmpg会压入1,fcmpl会压入-1。 dcmpg,dcmpl同理
创建指令
- 创建类实例指令: new
它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈 - 创建数组指令:newarray、anewarray、multianewarray
newarray:创建基本类型数组
anewarray:创建引用类型数组
multianewarray :创建多维数组
public void newTest() {
Object obj = new Object();
File file = new File("test");
}
public void newArray() {
int[] intArr = new int[10];
Object[] objArr = new Object[10];
int[][] intArr2 = new int[10][11];
String[][] strArr2 = new String[10][];
String[][] strArr3 = new String[10][11];
}
newTest
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: new #3 // class java/io/File
11: dup
12: ldc #4 // String test
14: invokespecial #5 // Method java/io/File."<init>":(Ljava/lang/String;)V
17: astore_2
18: return
newArray
0: bipush 10
2: newarray int
4: astore_1
5: bipush 10
7: anewarray #2 // class java/lang/Object
10: astore_2
11: bipush 10
13: bipush 11
15: multianewarray #6, 2 // class "[[I"
19: astore_3
20: bipush 10
22: anewarray #7 // class "[Ljava/lang/String;"
25: astore 4
27: bipush 10
29: bipush 11
31: multianewarray #8, 2 // class "[[Ljava/lang/String;"
35: astore 5
37: return
数组操作指令
·把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
·将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
数组类型 | 加载指令 | 存储指令 |
---|---|---|
byte/boolean | baload | bastore |
char | caload | castore |
short | saload | sastore |
int | iaload | iastore |
long | laload | lastore |
float | faload | fastore |
double | daload | dastore |
reference | aaload | aastore |
说明
指令xaload表示将数组的元素压入栈,比如·saload、caload分别表示压入short数组和char数组。指令xaload在执行时,要求操作数栈顶元素为数组索引i,栈顶顺位第2个元素为数组引用a,该指令会弹出栈顶这两个元素,并将a[i]重新压入栈。
xastore则专门针对数组操作,以iastore为例,它用于给一个int数组的给定索引赋值。在iastore执行前,操作数栈顶需要以此准备3个元素:值、索引、数组引用,iastore会弹出这3个值,并将值赋值给数组指定索引的位置。
public void arrayTest() {
int[] arr = new int[10];
arr[3] = 20;
System.out.println(arr[1]);
}
0: bipush 10
2: newarray int
4: astore_1
5: aload_1
6: iconst_3
7: bipush 20
9: iastore
10: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
13: aload_1
14: iconst_1
15: iaload
16: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
19: return
取数组长度的指令:arraylength
弹出栈顶数组元素获得其长度并压入栈
public void arrayTest() {
int[] arr = new int[10];
System.out.println(arr.length);
}
0: bipush 10
2: newarray int
4: astore_1
5: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
8: aload_1
9: arraylength
10: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
13: return
类型检查指令
检查类实例类型的指令:instanceof、checkcast
- checkcast:用于检查强制转换是否可以进行。如果可以进行,那么checkcast 指令不会改变操作数栈,否则它会抛出ClassCastException异常
- instanceof:用来判断给顶对象是否是某一个类的实例,它会判断结果是否压入操作数栈
public String fun(Object obj) {
if (obj instanceof Object) {
return (String) obj;
}
else {
return null;
}
}
0: aload_1 ---->将参数obj入栈
1: instanceof #6 // class java/lang/Object
4: ifeq 12 ---->如果符合跳到12执行
7: aload_1
8: checkcast #7 // class java/lang/String
11: areturn ---->引用类型返回
12: aconst_null
13: areturn
方法返回指令
包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用
说明
通过ireturn指令,将当前操作数栈顶元素弹出,并将这个元素压入调用者的操作数栈中,所有当前操作数栈中的其他元素都会被丢弃。
如果当前返回的是synchronized方法,那么还会执行一个隐藏的monitorexit指令,退出临界区。
最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者。
public void fun() {
int a = 1;
int b = doubleInt();
}
public int doubleInt() {
int i = 10;
return i;
}
fun():
0: iconst_1
1: istore_1
2: aload_0 ---->this入栈
3: invokevirtual #6 // Method doubleInt:()I ---->开启一个新栈帧doubleInt方法,运行完成后栈顶会有doubleInt的返回值
6: istore_2
7: return
doubleInt():
0: bipush 10
2: istore_1
3: iload_1
4: ireturn
操作数栈管理指令
- 将操作数栈的栈顶一个或两个元素出栈并废弃:pop、pop2
- 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
- 将栈最顶端的两个slot互换,目前没有交换两个64位数据(long、double)类型的指令:swap
- nop,是一个非常特殊的指令,它的字节码为0x00。和汇编语言中的nop一样,它表示什么都不做。这条指令一般可用于调试、占位等
说明
- 不带_x的指令是复制栈顶数据并压入栈顶。包括两个指令,dup和dup2。dup的系数代表要复制的Slot数。
- dup开头的指令用于复制1个slot的数据。例如1个int或1个reference类型数据。
- dup2开头的指令用于复制2个slot的数据。例如1个long或2个int类型数据。
- 带_x的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令,dup_x1, dup2_x1, dup_x2, dup2_x2。对于带_x的复制插入指令,只要将指令的dup和x的系数相加,结果即为需要插入的位置。
- dup_x1插入位置:1+1=2,即栈顶2个slot下面
- dup_x2插入位置:1+2=3,即栈顶3个slot下面
- dup2_x1插入位置:2+1=3,即栈顶3个slot下面
- dup2_x2插入位置:2+2=4,即栈顶4个slot下面
- pop:将栈顶的1个slot数值出栈。例如1个short类型数值
- pop2:将栈顶的2个slot数值出栈。例如1个double类型数值或2个int类型
条件跳转指令
指令 | 说明 |
---|---|
ifeq | 当栈顶int型数值等于0时跳转 |
ifne | 当栈顶int型数值不等于0时跳转 |
iflt | 当栈顶int型数值小于0时跳转 |
ifge | 当栈顶int型数值大于等于0时跳转 |
ifgt | 当栈顶int型数值大于0时跳转 |
ifle | 当栈顶int型数值小于等于0时跳转 |
ifnull | 为null时跳转 |
ifnonnull | 不为null时跳转 |
if_icmpeq | 比较栈顶两int型数值大小,当结果等于0时跳转 |
if_icmpne | 比较栈顶两int型数值大小,当结果不等于0时跳转 |
if_icmplt | 比较栈顶两int型数值大小,当结果小于0时跳转 |
if_icmpgt | 比较栈顶两int型数值大小,当结果大于0时跳转 |
if_icmple | 比较栈顶两int型数值大小,当结果小于等于0时跳转 |
if_icmpge | 比较栈顶两int型数值大小,当结果大于等于0时跳转 |
if_acmpeq | 比较栈顶两引用型数值,当结果相等时跳转 |
if_acmpne | 比较栈顶两引用型数值,当结果不相等时跳转 |
条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般就可以先用比较指令进行栈顶元素的准备,然后进行条件跳转。
条件跳转指令1:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull。这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)
它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置
说明
- 对于boolean、byte、char、short类型的条件分支比较操作,都是使用int类型的比较指令完成
- 对于long、float、double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整数型值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转
条件跳转指令2:if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne
多条件分支跳转指令
多条件分支跳转指令是专为switch-case语句设计的,主要有tableswitch和lookupswitch。
指令名称 | 描述 |
---|---|
tableswitch | 用于switch条件跳转,case值连续 |
lookupswitch | 用于switch条件跳转,case值不连续 |
· tableswitch要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率高。
- lookupswitch内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低。lookupswitch处理的是离散的case值,但出于效率考虑,将case-offset对按照case值大小进行排序。
public void fun() {
int num = 10;
switch (num) {
case 5:
num = 1;
break;
case 1:
num = 5;
break;
case 10:
num = -1;
break;
}
}
0 bipush 10
2 istore_1
3 iload_1
4 lookupswitch 3
1: 45 (+41)
5: 40 (+36)
10: 50 (+46)
default: 52 (+48)
40 iconst_1
41 istore_1
42 goto 52 (+10)
45 iconst_5
46 istore_1
47 goto 52 (+5)
50 iconst_m1
51 istore_1
52 return
jdk新特性
public void fun() {
String str = "aaa";
switch (str) {
case "aaa":
break;
case "bbb":
break;
case "ccc":
break;
}
}
0 ldc #2 <aaa>
2 astore_1
3 aload_1
4 astore_2
5 iconst_m1
6 istore_3
7 aload_2
8 invokevirtual #3 <java/lang/String.hashCode>
11 lookupswitch 3 ---->先根据hasecode值进行排序
96321: 44 (+33)
97314: 58 (+47)
98307: 72 (+61)
default: 83 (+72)
44 aload_2
45 ldc #2 <aaa>
47 invokevirtual #4 <java/lang/String.equals> ---->再用equals判断是否相等
50 ifeq 83 (+33)
53 iconst_0
54 istore_3
55 goto 83 (+28)
58 aload_2
59 ldc #5 <bbb>
61 invokevirtual #4 <java/lang/String.equals>
64 ifeq 83 (+19)
67 iconst_1
68 istore_3
69 goto 83 (+14)
72 aload_2
73 ldc #6 <ccc>
75 invokevirtual #4 <java/lang/String.equals>
78 ifeq 83 (+5)
81 iconst_2
82 istore_3
83 iload_3
84 tableswitch 0 to 2 0: 112 (+28)
1: 115 (+31)
2: 118 (+34)
default: 118 (+34)
112 goto 118 (+6)
115 goto 118 (+3)
118 return
无条件跳转
目前主要的无条件跳转指令为goto。指令goto接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。
如果指令偏移量太大,超过双字节的带符号整数范围,则可以使用goto_w,它和goto有相同的作用,但是它接收4个字节的操作数,可以表示更大的地址范围。
指令jsr、jsr_w、ret虽然也是无条件跳转,但主要用于try-finally语句,且已经被虚拟机逐渐废弃
指令名称 | 描述 |
---|---|
goto | 无条件跳转 |
goto_w | 无条件跳转(宽索引) |
jsr | 跳转到指定16位offset位置,并将jsr下一条指令地址压入栈顶 |
jsr_w | 跳转到指定32位offset位置,并将jsr_w下一条指令地址压入栈顶 |
ret | 返回至由指定的局部变量所给出的指令位置(一般与jsr、jsr_w联合使用) |
抛出异常指令
在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除了用throw语句显式抛 出异常的情况之外,《Java虚拟机规范》还规定了许多运行时异常会在其他Java虚拟机指令检测到异常 状况时自动抛出。例如前面介绍整数运算中,当除数为零时,虚拟机会在idiv或ldiv指令中抛出 ArithmeticException异常。 而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和 ret指令来实现,现在已经不用了),而是采用异常表来完成。
正常情况下,操作数栈的压入都是一条条指令完成的。唯一的例外情况是在抛异常时,java虚拟机会清除操作数栈上的所有内容,而后将异常压入操作数栈上。
异常及异常的处理:
过程一:异常对象的生成---->throw ---->指令:athrow
过程二:异常的处理:抓抛模型---->throw---->使用异常表
public void fun(int i) {
if (i == 0) {
throw new RuntimeException("i==0");
}
}
0: iload_1
1: ifne 14
4: new #6 // class java/lang/RuntimeException
7: dup
8: ldc #7 // String i==0
10: invokespecial #8 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
13: athrow
14: return
同步指令
java虚拟机支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor(更常见的是直接将它称为“锁”)来支持的。
方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟 机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为 同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如 果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成 还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取 到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同 步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。
同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中 有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字 需要Javac编译器与Java虚拟机两者共同协作支持。
当一段代码进入同步代码块时,它使用monitorenter指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入。若为1.则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块。
当前线程退出同步块时,需要使用monitorexit声明退出。在java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。
void onlyMe(Foo f) {
synchronized(f) {
doSomething();
}
}
Method void onlyMe(Foo)
0 aload_1 // 将对象f入栈
1 dup // 复制栈顶元素(即f的引用)
2 astore_2 // 将栈顶元素存储到局部变量表变量槽 2中
3 monitorenter // 以栈定元素(即f)作为锁,开始同步
4 aload_0 // 将局部变量槽 0(即this指针)的元素入栈
5 invokevirtual #5 // 调用doSomething()方法
8 aload_2 // 将局部变量Slow 2的元素(即f)入栈
9 monitorexit // 退出同步
10 goto 18 // 方法正常结束,跳转到18返回
13 astore_3 // 从这步开始是异常路径,见下面异常表的Taget 13
14 aload_2 // 将局部变量Slow 2的元素(即f)入栈
15 monitorexit // 退出同步
16 aload_3 // 将局部变量Slow 3的元素(即异常对象)入栈
17 athrow // 把异常对象重新抛出给onlyMe()方法的调用者
18 return // 方法正常返回
Exception table:
FromTo Target Type
4 10 13 any
13 16 13 any