7. 字节码指令详解

字节码指令一般包括两种形式:一种是只有一个操作符,一种是操作符+操作数。下面字节码指令中会频繁遇到操作数栈和局部变量表,关于两者的用途可以参考
JVM运行时数据区

1 加载与存储指令

加载指的是从栈帧中的局部变量表中加载数据到栈帧中的操作数栈中,存储与加载正好反过来,指的是把操作数栈中的数据存储到局部变量表中。关于局部变量表和操作数栈的理解可以参考JVM的运行时数据区

1.1 加载

加载又包括两种加载,第一:从局部变量表中加载数据到操作数栈中;第二种:直接把常量加载到操作数栈中。

1. 从局部变量表中加载数据到操作数栈中
从局部变量表中加载数据到操作数栈中主要包括两类指令 xload mxload_<n>,其中x=i(int类型)、l(long类型)、f(float类型)、d(double类型)、a(引用类型); n=0、1、2、3; m>=4。
例如:
iload_<0> :表示把局部变量表中第0个元素加载到操作数栈中,该元素为int类型;
lload_<1>:表示把局部变量表中第1个元素加载到操作数栈中,该元素为long类型;
aload_<3>:表示把局部变量表中第3个元素加载到操作数栈中,该元素为引用类型;
dload 4:表示把局部变量表中第4个元素加载到操作数栈中,该元素为double类型;

为什么有了xload_<n>,还需要xload m
因为xload_<n>只能加载前4个局部变量表中数据到操作数栈中,在一个方法中,一般都是局部变量少于4个, 所以xload_<n>用一个字节就可以把局部变量表中数据加载到操作数栈中。如果用比如iload 0把第0个局部变量加载到操作数栈中,还需要额外的2个字节来表示操作数,耗费内存,因此对于前4个局部变量表,用xload_<n>更节省内存。

以下面loadTest方法为例

public void loadTest(int num, Object o, long count, boolean flag, short[] arrs){
    System.out.println(num);
    System.out.println(o);
    System.out.println(count);
    System.out.println(flag);
    System.out.println(arrs);
}

把该loadTest方法解析成JVM字节码指令如下所示

 0 getstatic #2 <java/lang/System.out>
 3 iload_1												#把num从局部变量表中加载到操作数栈中
 4 invokevirtual #3 <java/io/PrintStream.println>
 7 getstatic #2 <java/lang/System.out>
10 aload_2												#把o从局部变量表中加载到操作数栈中
11 invokevirtual #4 <java/io/PrintStream.println>
14 getstatic #2 <java/lang/System.out>
17 lload_3												#把count从局部变量表中加载到操作数栈中
18 invokevirtual #5 <java/io/PrintStream.println>
21 getstatic #2 <java/lang/System.out>
24 iload 5 												#把flag从局部变量表加载到操作数栈中
26 invokevirtual #6 <java/io/PrintStream.println>
29 getstatic #2 <java/lang/System.out>
32 aload 6 												#把arrs从局部变量表中加载到操作数栈中
34 invokevirtual #4 <java/io/PrintStream.println>
37 return

2. 直接把常量加载到操作数栈中
加载常量到操作数栈中指令通常有:bipushsipushldcldc_wldc2_waconst_nulliconst_mliconst_<i>lconst_<l>fconst_<f>dconst_<d>。加载常量到操作数栈又可以分为3中类型的加载指令:const系列、push系列、ldc系列。

const系列
加载入栈的常量包含在指令本身中。指令有:iconst_<i>(i从0到5)、iconst_mllconst_<l>(l从0到1)、fconst_<f>(f从0到2)、dconst_<d>(d从0到1)、aconst_null

指令用法
iconst_ml:将-1加载到操作数栈;
iconst_i:将i加载到操作数栈,i取值从0到5;
lconst_0、lconst_1:分别将长整数0和1加载到操作数栈;
fconst_0、fconst_1、fconst_2:分别将浮点数0.0、1.0、2.0加载到操作数栈中;
dconst_0、dconst_1:分别将double型的0和1加载到操作数栈中;
aconst_null:将null加载到操作数栈中。

push系列
由于上面iconst最大只能把5加载到操作数栈,如果大于5的就不能用上面的命令进行加载常量。大于5的整数可以用push系列的命令进行加载。主要包括bipushsipush,两者处理的整数范围不同,bipush加载8位的整数到操作数栈中;sipush加载16位整数到操作数栈中。注意bipushsipush处理的是整数。

ldc系列
如果以上指令都不满足要求,可以使用万能的ldc指令,它可以加载一个8位的参数到操作数栈中,要加载的参数是指向常量池中int、floag或者String的索引,将指定的常量池中的常量加载到操作数栈中。
ldc_w加载两个8位参数到操作数栈中,能支持索引的范围大于ldc
如果要加载的元素是long或者double类型的,则使用ldc2_w指令。

案例一:以constPushLdcTest为例,对比const、push和ldc系列的整数常量加载情况

public void constPushLdcTest(){
    int a = -1;
    int b = 5;
    int c = 6;
    int d = 127;
    int e = 128;
    int f = 32767;
    int g = 32768;
}

把该方法解析成JVM字节码指令如下所示

 0 iconst_m1		#把-1加载到操作数栈中
 1 istore_1
 2 iconst_5			#把5加载到操作数栈中
 3 istore_2
 4 bipush 6			#把6加载到操作数栈中
 6 istore_3
 7 bipush 127 		#把127加载到操作数栈中
 9 istore 4
11 sipush 128 		#把128加载到操作数栈中	
14 istore 5
16 sipush 32767 	#把32767加载到操作数栈中
19 istore 6
21 ldc #7 <32768> 	#把常量池中索引为7的常量加载到操作数栈中,常量池中索引为7表示常量32768
23 istore 7
25 return

案例二:以constLdcTest为例,对比int类型外的其他常量加载方式

public void constLdcTest(){
    long a = 1;
    long b = 2;
    float c = 2;
    float d = 3;
    double e = 1;
    double f = 2;
    Date g = null;
}

把该方法解析成JVM字节码指令如下所示

 0 lconst_1 			#把long类型为1的数加载到操作数栈中
 1 lstore_1
 2 ldc2_w #8 <2>		#把常量池中索引8的常量(常量值为2)加载到操作数栈中
 5 lstore_3
 6 fconst_2 			#把float类型为2的数加载到操作数栈中
 7 fstore 5
 9 ldc #10 <3.0> 		#把常量池中索引为10的常量(常量值为3.0)加载到操作数栈中
11 fstore 6
13 dconst_1 			#把double类型为1的数加载到操作数栈中
14 dstore 7
16 ldc2_w #11 <2.0> 	#把常量池中索引为11的常量(常量值为2.0)加载到操作数栈中
19 dstore 9
21 aconst_null 			#把null加载到操作数栈中
22 astore 11
24 return

综上,3种系列的常量加载指令,加载的范围不同,对比如下:
在这里插入图片描述

1.2 存储

将操作数栈中的数据存储到局部变量表中指令通常有:xstore(其中x=i、l、f、d、a)、xstore_<n>(其中x=i、l、f、d、a; n=0、1、2、3)
xstore_<n>
其中xstore_n 只能把操作数栈中的数据存储到局部变量表中第0到第3号槽位。
istore把操作数栈中的int类型数据存储到局部变量表中;
lstore把操作数栈中的long类型数据存储到局部变量表中;
fstore把操作数栈中的float类型数据存储到局部变量表中;
dstore把操作数栈中的double类型数据存储到局部变量表中;
astore把操作数栈中的引用类型数据存储到局部变量表中;
案例:以如下java源码为例

public void storeTest(int k, double d){
    k = 10;
    long l = 20;
    String str = "hello world";
    float f = 30f;
    d = 40;
}

该方法解析成JVM指令如下所示

 0 bipush 10 				
 2 istore_1					#把int类型的数据10存储到局部变量表中索引为1的位置(更新索引为1处的值)
 3 ldc2_w #13 <20>
 6 lstore 4 				#把long类型的数据20存储到局部变量表中索引为4的位置(更新索引为4的值)
 8 ldc #15 <hello world>
10 astore 6					#把引用类型的hello world存储到局部变量表中索引为6的位置,因为变量l是long型的,占据了第4和第5号位置。
12 ldc #16 <30.0>
14 fstore 7 				#把float类型的数据30存储到局部变量表中索引为7的位置
16 ldc2_w #17 <40.0>
19 dstore_2 				#把double类型的数据40存储到局部变量表中索引为2的位置(更新索引23的位置,因为doule类型占局部变量表中2个槽位)
20 return

2 运算指令

在下面的的运算指令中,i开头的指令表示对int类型数据进行运算,l开头指令表示对long类型数据运算,f开头的指令表示对float类型数据运算, d开头的指令表示对double类型数据运算。
加法指令:iaddladdfadddadd
减法指令:isublsubfsubdsub
乘法指令:imullmulfmuldmul
除法指令:idivldivfdivddiv
求余指令:iremlremfremdrem; //remainder 余数
求反指令:ineglnegfnegdneg; //negation 取反
自增指令:iinc
位运算指令有可分为如下:
位移指令:ishlishriushrlshllshrlushr
位或指令:iorlor
位与指令:iandland
位异或指令:ixorlxor

2.1 求反指令示例

以如下java源码为例

public void negTest(){
    int a = 10;
    float b = 20.0F;
    long c = 30;
    double d = 40.0;
    int a1 = -a;
    float b1 = -b;
    long c1 = -c;
    double d1 = -d;
}

将该java源码解析成JVM指令如下所示

 0 bipush 10
 2 istore_1
 3 ldc #13 <20.0>
 5 fstore_2
 6 ldc2_w #14 <30>
 9 lstore_3
10 ldc2_w #16 <40.0>
13 dstore 5
15 iload_1		#1.把局部变量表中1号索引位置的int型数据加载到操作数栈栈顶
16 ineg			#对栈顶元素取反操作,即对整数10取反
17 istore 7		#把整数10取反后的数据再到局部变量表中7号索引位置
19 fload_2		#2.把局部变量表中2号索引位置的float型数据加载到操作数栈栈顶
20 fneg 		#对栈顶元素取反操作,即对float型数据20.0取反
21 fstore 8		#把float类型的20取反后的数据存储到局部变量表中8号索引位置
23 lload_3		#3.把局部变量表中3号索引位置的long类型数据加载到操作数栈栈顶
24 lneg			#堆栈顶元素取反操作,即对long型数据30取反
25 lstore 9		#把long型的30取反后的数据存储到局部变量表中索引9的位置
27 dload 5		#4.把局部变量表中5号索引位置的double类型数据加载到操作数的栈顶
29 dneg			#堆栈顶元素取反操作,即对double类型的40取反
30 dstore 11	#把double型的40取反后的数据存储到局部变量表中索引为11的位置
32 return

2.2 加法指令示例

以如下java源码为例

public void addTest(){
    int a = 10;
    int b = 20;
    int c = a + b;
}

解析成JVM指令如下所示

 0 bipush 10
 2 istore_1
 3 bipush 20
 5 istore_2
 6 iload_1
 7 iload_2
 8 iadd		#对栈顶的两个int类型数据执行加法操作,执行该步骤时会把栈顶的2int数据执行出栈,然后把两者的和压入操作数栈
 9 istore_3
10 return

2.3 乘法指令示例

以如下java源码为例

public void mulTest(){
    int a = 10;
    int b = 20;
    int c = a * b;
}

解析成JVM指令如下

 0 bipush 10
 2 istore_1
 3 bipush 20
 5 istore_2
 6 iload_1
 7 iload_2
 8 imul		#对栈顶的两个int类型数据执行乘法操作,执行该步骤时会把栈顶的2int数据执行出栈,然后把两者的积压入操作数栈
 9 istore_3
10 return

2.4 位运算指令示例

以如下java代码为例

public void bitArithmeticTest(){
    int a = 2;
    int b = 3;
    int c = a & b;
    int d = a ^ b;
}

解析成JVM指令如下所示

 0 iconst_2
 1 istore_1
 2 iconst_3
 3 istore_2
 4 iload_1
 5 iload_2
 6 iand			#对操作数栈顶的2int型数据执行位与操作,同时对此2int型数据执行出栈操作
 7 istore_3
 8 iload_1
 9 iload_2
10 ixor 		对操作数栈顶的2int型数据执行位异或操作,同时对此2int型数据执行出栈操作
11 istore 4
13 return

3 类型转换

类型转换指令用于将两种不同的数值类型进行相互转换。 类型转化分为宽化类型转换(宽化类型转换又称为向上转型)和窄化类型转换(窄化类型转换又称为向下转型)。

3.1 宽化类型转换

宽化类型转换也叫向上类型转换,用于将小范围类型向大范围类型转换的安全转换。

转换规则为:int -> long -> float -> double

  • 从int类型向long、float、double类型转换,对应的指令分别为:i2li2fi2d
  • 从long类型向float、double类型转换,对应的指令分为为:l2fl2d
  • 从float类型向double类型转换,对应的指令分为为:f2d

向上类型转化时需要注意如下2个问题;

  1. 精度损失问题
    从int向long类型转换,或者从int向double类型转换,是不会丢失任何信息的,转换后的值是精确相等的;
    从int、long向float转换,或者long类型向double类型转换,可能发生精度丢失,丢失几个最低有效位上的值,转换后的浮点数值根据IEEE754最接近舍入模式所得到的数值;
    尽管向上转化类型有可能发生精度丢失,但是这种转换不会发生java虚拟机抛出运行时异常。

  2. byte、char、short类型转换问题
    byte、char、short转换为int类型时是不存在类型转换的,因为底层byte、char、short都是按照int类型存储的,在局部变量表中都是占用一个槽位,一个槽位4个字节,32位。比如byte类型向上转换为long类型时与int向上转化为long类型用的命令都是i2l。

以如下java源码为例

public void int2floatTest(){
    int i = 1234567890;
    float f = i;
    System.out.println(f);
}

执行该方法,结果输出1.23456794E9,由此可见,int类型转化为float精度丢失了。
该方法解析成JVM指令如下

 0 ldc #22 <1234567890>
 2 istore_1
 3 iload_1
 4 i2f			#int类型向上转型为float类型
 5 fstore_2
 6 getstatic #2 <java/lang/System.out>
 9 fload_2
10 invokevirtual #23 <java/io/PrintStream.println>
13 return

3.2 窄化类型转换

窄化类型转换也叫强制类型转化,强制类型转化支持以下形式:

  • 从int类型向byte、short、char类型转换,对应指令为i2bi2si2c
  • 从long类型向int类型转换,对应指令为l2i;
  • 从float类型向int或者long类型转换,对应的指令为f2if2l
  • 从double类型向int、long、float类型转换,对应指令为d2id2ld2f

强制类型转换需要注意以下问题:

  1. 强制类型转换可能发生上限溢出、下限溢出、精度丢失问题,但java虚拟机规定强制类型转换不会导致虚拟机抛出异常。
  2. double类型强制转换为int或者long类型

    如果浮点值是NaN,转换结果为int或者long型的0;
    如果浮点值不是无穷大的话,浮点值使用IEEE754的舍入模式只取浮点数的整数;
    如果double类型是无穷大时,取int或者long的无穷大

  3. double类型强制转换为float类型

    由于double的精度远远高于float的精度,如果转换结果的绝对值太小而无法使用float来表示,将返回float类型的正负0;
    如果转换结果的绝对值太大而无法使用float来表示,则返回float类型的正负无穷大;
    double类型的NaN值转换为float类型的NaN值。

以如下java源码为例

public void double2AllTest(){
    double d = Double.POSITIVE_INFINITY;
    long l = (long)d;
    int i = (int) d;
    float f = (float) d;
    System.out.println(d);
    System.out.println(l);
    System.out.println(i);
    System.out.println(f);
}

运行该方法输出结果如下

Infinity
9223372036854775807
2147483647
Infinity

其中9223372036854775807表示long类型的最大值,2147483647表示int类型的最大值,Infinity表示无穷大。由此可见double类型的无穷大转换为Int和long型的最大值,转化为float类型的无穷大值。
该方法解析为JVM指令如下:

 0 ldc2_w #25 <Infinity>
 3 dstore_1
 4 dload_1
 5 d2l			#double强转为long类型
 6 lstore_3
 7 dload_1
 8 d2i 			#double强转为int类型
 9 istore 5
11 dload_1
12 d2f 			#double强转为float类型
13 fstore 6
15 getstatic #2 <java/lang/System.out>
18 dload_1
19 invokevirtual #27 <java/io/PrintStream.println>
22 getstatic #2 <java/lang/System.out>
25 lload_3
26 invokevirtual #5 <java/io/PrintStream.println>
29 getstatic #2 <java/lang/System.out>
32 iload 5
34 invokevirtual #3 <java/io/PrintStream.println>
37 getstatic #2 <java/lang/System.out>
40 fload 6
42 invokevirtual #23 <java/io/PrintStream.println>
45 return

4 对象的创建与访问指令

对象的创建与访问指令可以分为对象创建指令、类或对象的字段访问指令、数组操作指令、类型检查指令。

4.1 对象创建指令

数组对象与类对象一样,在java源码中,都是通过new的方式进行创建的,但在字节码指令中是通过不同的指令进行创建的。

创建类对象指令
通过new 指令 + 操作数(指向常量池中的索引,表示要创建对象的类型)进行创建类对象实例,执行完成后,将对象的引用压入操作数栈。

创建数组对象指令

  • newarray:创建基本数组对象;
  • anewarray:创建引用类数组对象;
  • multianewarray:创建多维数组对象。
以如下java源码为例
    public void newObjectTest(){
        Persion persion = new Persion();
        int[] intArray = new int[5];
    }

解析成jvm字节码指令如下所示

 0 new #30 <com/lzj/classes/Persion>	#new 一个persion对象,并把persion对象的地址压入操作数栈
 3 dup 									#把栈顶的persion对象的地址复制一份,压入操作数栈,此时栈顶以及栈顶次位的数据均为persion对象的地址
 4 invokespecial #31 <com/lzj/classes/Persion.<init>>	#调用persion的init方法,消耗栈顶的一个persion对象
 7 astore_1 							#把另一个prsion对象地址存储到局部变量表中索引为1的位置
 8 iconst_5								#加载常整数5到操作数栈
 9 newarray 10 (int) 					#new一个长度为5int类型的数组对象,并且常整数5出栈,数据对象压入栈
11 astore_2 							#把栈顶数组对象的地址存储到局部变量表中索引为2的位置
12 return

4.2 类或对象的字段访问指令

对象创建后,就可以通过对象访问对象中的字段(field,也叫字段属性),或者也可以通过类直接访问字段。

  • 通过对象访问字段:getfieldputfield
  • 通过类访问字段(字段为static类型):getstatic、putstatic。

getfiledputfield为例
getfield + 操作数(操作数为指向常量池中的索引),它的作用表示获取对象的filed字段加载到操作数栈中;
putfiled+操作数(操作数为指向常量池中的索引),它的作用表示把操作数栈顶的数据存放到对象的filed字段中,并对栈顶的数据执行出栈操作。

以如下java源码为例

public void vistFieldTest(){
    Persion persion = new Persion();
    persion.id = 2;
}

解析成jvm字节码指令为

 0 new #30 <com/lzj/classes/Persion>
 3 dup
 4 invokespecial #31 <com/lzj/classes/Persion.<init>>
 7 astore_1
 8 aload_1
 9 iconst_2										#常整数2加载到操作数栈中
10 putfield #32 <com/lzj/classes/Persion.id>	##32代表常量池中field字段,把操作数栈顶元素2赋值给field字段
13 return

4.3 数组操作指令

关于数组的操作主要分为如下两类指令:xaloadxastore。其中x取值如下图所示,不同的数组类型,x取值不同。
在这里插入图片描述

ialoadiastore为例
iaload:数组对象是存储在堆中的,iaload表示把堆中数组对象中的int数据加载到操作数栈中。用法iaload + 操作数,操作数必须是下表中对应类型的值,比如int数组对应的是10,即iaload 10。
iastore:表示把int数据存放到堆中数组对象中。用法例如 iastore 10
在这里插入图片描述

另外,数组操作指令除了xaloadxastore,还有一个计算数组长度的指令arraylength

以如下java源码为例

public void arrayTest(){
    int[] intArray = new int[3];
    intArray[1] = 1;

    byte[][] byteArray = new byte[3][3];
    byteArray[1][1] = 1;

    System.out.println(intArray.length);
}

解析成jvm字节码如下所示

 0 iconst_3							#把常量3压入栈顶
 1 newarray 10 (int)				#创建长度为3int类型数组对象,并且常量3和数组对象的地址出栈
 3 astore_1							#把刚创建的int类型数组对象地址存储到局部变量表中索引为1的位置
 4 aload_1							#把刚创建的int类型数组对象地址存从局部变量表中加载到操作数栈
 5 iconst_1							#把常量1压入栈
 6 iconst_1							#把常量1压入栈
 7 iastore							#把整数1赋值给int类型数组索引为1的位置,然后栈中的数组对象地址和21分别出栈
 8 iconst_3							#3压入操作数栈
 9 iconst_3 						#3压入操作数栈
10 multianewarray #28 <[[B> dim 20 	#创建一个byte类型的二维数组对象,并把二维数组对象地址压入操作数栈
14 astore_2							#把刚创建的byte类型的二维数组存储到局部变量表中索引为2的位置,并且二维数组对象地址出栈
15 aload_2 							#把二维数组对象地址加载到操作数栈中
16 iconst_1							#常量1压入栈			
17 aaload							#获取二维数组索引为1位置处数据,也即为一个一维的数组对象地址
18 iconst_1
19 iconst_1
20 bastore							#把这个一维数组的角标为1的位置赋值1,也即二维数组[1][1]处赋值1
21 getstatic #2 <java/lang/System.out>
24 aload_1
25 arraylength 						#计算最上面int类型数组的长度
26 invokevirtual #3 <java/io/PrintStream.println>
29 return

4.4 类型检查指令

用于检查对象的类型指令,包括:instanceofcheckcast

  • instanceof:用来判定给定的对象是否某一个类的实例,然后将判断结果压入操作数栈;
  • checkcast:用于判断强制类型转换是否可进行,如果可以进行,checkcast不会该表操作数栈中内容,如果不可以进行,则抛出ClassCastException异常。

如以下java源码为例

public String castTest(Object obj){
    if (obj instanceof String){
        return (String) obj;
    }
    return null;
}

解析成jvm字节码指令如下所示

 0 aload_1								#把局部变量表中索引为1的数据,也即obj对象的地址加载到栈中
 1 instanceof #29 <java/lang/String>	#判断obj是否String的实例,并obj出栈
 4 ifeq 12 (+8)
 7 aload_1								#如果obj是String的实例,把obj对象的地址加载到栈中
 8 checkcast #29 <java/lang/String>		#把obj强转为String类型的对象,并且obj出栈
11 areturn
12 aconst_null			
13 areturn

5 方法调用指令

方法调用主要分为以下几个指令:

  • invokespecial :调用对象中不会动态派发的方法,包括构造器方法、私有方法、父类方法。比如调用构造方法,就是调用指定类的指定构造方法,不会是父类中的构造方法;再比如调用私有方法,由于私有方法不会被重写,因此调用某个类中的私有方法,也是指定的一个方法;调用public方法不可以是invokespecial,因为public 方法可以被重写,调用子类的一个public方法,有可能是调用的父类的该public 方法(是从父类中继承的方法);
  • invokeinterface:调接口中的方法,动态执行时会调用实际实现类中的方法;
  • invokestatic:调用类中static方法;
  • invokevirtual:除了以上3中调用,基本都是invokevirtual了,在调用一个对象的方法时,会根据实际类型进行动态分配,支持多态;
  • invokedynamic:JDK7之后添加的指令,表示要调用的目标方法是没法提前预知,是根据动态传进的方法进行调用的,比如lambda表达式。

以如下java源码为例

public void invokeTest(){
    Persion persion = new Persion();
    persion.setId(1);
}

解析成jvm字节码指令如下

 0 new #31 <com/lzj/classes/Persion>
 3 dup
 4 invokespecial #32 <com/lzj/classes/Persion.<init>>	#调用perison对象的构造器方法,由于构造器方法是一定的,故invokespecial
 7 astore_1
 8 aload_1
 9 iconst_1
10 invokevirtual #34 <com/lzj/classes/Persion.setId> 	#如果有子类继承persion,那么调用的setId方法有可能是persion子类的setId方法
13 return

6 方法返回指令

方法返回指令包括以下几个命令:

  • ireturn:用于方法返回值为boolean、byte、char、short、int类型;
  • lreturn:用于方法返回值为long类型;
  • freturn:用于方法返回值为float类型;
  • dreturn:用于方法返回值为double类型;
  • areturn:用于方法返回值为引用类型;
  • return:用于方法返回值为void类型。

以如下java源码为例

public long returnTest(int num){
    return num;
}

解析成JVM字节码指令如下

0 iload_1	#局部变量表中索引为1位置的数据加载到操作数栈,即num的值
1 i2l 		#num转化为long类型,num出栈,转化后的long类型入栈
2 lreturn	#栈顶的long值返回

7 操作数栈管理指令

操作数栈管理指令非用户java源码直接解析成的JVM字节码指令,而是虚拟机自动生成用于管理虚拟机操作数栈的,主要包括以下几种管理指令:

1. 出栈指令
pop将栈顶一个slot槽位的数据出栈,比如int类型、byte类型的数据;
pop2将栈顶2个slot槽位的数据出栈,比如double类型,或者2个int类型的数据;

以如下java源码为例

public void popTest(){
    Persion persion = new Persion();
    persion.toString();
}

解析成JVM字节码指令如下所示

 0 new #30 <com/lzj/classes/Persion>	#new一个persion对象,压入操作数栈
 3 dup									#复制一个persion对象,压入操作数栈
 4 invokespecial #31 <com/lzj/classes/Persion.<init>>	#消耗掉操作数中的一个persion对象,用于调用init方法
 7 astore_1								#把操作数栈中perison对象存储到局部变量表索引为1号位置
 8 aload_1								#局部变量表中的persion对象加载到操作数栈中
 9 invokevirtual #33 <java/lang/Object.toString> #消耗掉操作数栈中一个persion对象,用于调用toString方法
12 pop									#操作数栈中还有一个persion对象,在return之前弹出栈
13 return

2. 复制指令
复制指令包括dupdup2dup_x1dup_x2dup2_x1dup2_x2。其中不带_x的复制指令表示复制操作数栈顶数据然后把复制的数据也压入操作数栈顶;带_x的复制指令表示复制操作数栈顶的数据,然后把复制的数据压入操作数栈顶下的某个位置,只需将指令的系数和x的系数相加,就位需要插入栈顶下的位置。

  • dup:表示复制操作数顶的一个slot位置的数据并压入操作数栈,比如int类型或者引用类型;
  • dup2:表示复制操作数顶的2个slot位置的数据并压入操作数栈,比如double类型、long类型、2个int类型等;
  • dup_x1:表示复制操作数栈顶的一个slot数据并插入距离栈顶2(dup的系数为1,x1的系数为1,1+1=2)个slot位置下面;
  • dup_x2:表示复制操作数栈顶的一个slot数据并插入距离栈顶3(dup的系数为1,x2的系数为2,1+2=3)个slot位置下面;
  • dup2_x1:表示复制操作数栈顶的2个slot数据并插入距离栈顶3(dup2的系数为2,x1的系数为1,2+1=3)个slot位置下面;
  • dup2_x2:表示复制操作数栈顶的2个slot数据并插入距离栈顶4(dup的系数为2,x2的系数为2,2+2=4)个slot位置下面;

案例一:以如下java源码为例

public void dupTest(){
    Persion persion = new Persion();
}

解析成JVM字节码指令如下所示

0 new #30 <com/lzj/classes/Persion>	#new一个persion对象,并把persion对象地址压入到操作数栈中
3 dup								#复制persion对象地址,压入操作数栈
4 invokespecial #31 <com/lzj/classes/Persion.<init>>	#调用操作栈顶persion对象的init方法,并且persion对象地址出栈
7 astore_1							#把另一个操作数栈中persion对象地址储存到局部变量表中
8 return

案例二:以如下java源代码为例

private long index = 0;

public long dup2Test(){
    return index++;
}

解析成JVM指令如下所示

 0 aload_0 	#把局部变量表中索引为0位置的this对象加载到操作数栈中
 1 dup 		#把操作数栈中的this对象复制一份压入操作数栈
 2 getfield #2 <com/lzj/classes/CodeDemo.index>	#消耗一份this对象,用于获取当前对象的index属性,并把index的值压入栈
 5 dup2_x1	#复制一份index插入到距离栈顶3个slot槽的位置处:由于栈顶的long类型的index占2个slot位置,紧接着this对象的地址占1个slot位置,然后把复制的index的值插入到this地址的下面,占2个slot位置
 6 lconst_1 #把long类型的1压入到栈顶
 7 ladd 	#把栈最上面的1和index的值相加并把结果压入栈,两个加数出栈
 8 putfield #2 <com/lzj/classes/CodeDemo.index> 	#此时栈顶为long类型的和1,紧接着this对象地址,把1赋值给this对象的index属性,两者都出栈
11 lreturn 	#此时栈中只剩下index的值,直接返回

另外还有2个不常用的指令
swap用于交换操作数栈最顶端的两个slot位置的数据,每个slot占4个字节,32位;
nop指令是非常特殊的一个指令,字节码为0x00,与汇编语言中的nop一样,表示什么都不做的意思,一般用于调试、占位等。

8 控制转移指令

为了支持条件控制,条件跳转,控制转移又分为以下几部分:比较指令、条件跳转指令、比较条件跳转指令、多条件分支跳转指令、无条件跳转指令等。

8.1 比较指令示例

比较指令主要包括:dcmpgdcmplfcmpgfcmpllcmp
dcmpgdcmpl是比较两个double类型数据;
fcmpgfcmpl是比较两个float类型数据;
lcmp是比较两个long类型数据。
可以看出,double类型和float类型的比较指令各有两个,是因为遇到NaN的值比较时处理方式不同。以fcmpgfcmpl为例:
fcmpgfcmpl都从栈中弹出两个操作数,并将它们进行比较,设栈顶的元素为v2,栈顶顺位第2位元素为v1,若v1=v2,则栈顶压入0,如v1>v2,则栈顶压入1,若v1<v2,则栈顶压入-1。两个指令不同之处在于,如果入到NaN值,fcmpg栈顶压入1,而fcmpl栈顶会压入-1。

注:NaN 用于处理计算中出现的错误情况,比如 0.0 除以 0.0 或者求负数的平方根。

以如下java为例

    @Test
    public void dcompTest(){
        double a = 1;
        double b = 2;
        System.out.println(a < b);
    }

解析成JVM指令如下所示

 0 dconst_1
 1 dstore_1
 2 ldc2_w #11 <2.0>
 5 dstore_3
 6 getstatic #2 <java/lang/System.out>
 9 dload_1
10 dload_3
11 dcmpg			#首先a变量的值1先压入栈,然后b的值2又压入栈,由于1<2,因此把-1压入栈,a和b的值分别出栈
12 ifge 19 (+7)
15 iconst_1
16 goto 20 (+4)
19 iconst_0
20 invokevirtual #6 <java/io/PrintStream.println>
23 return

8.2 条件跳转指令

从8.1节案例可以看出,dcmpg只是进行了值比较,然后把比较的结果压入操作数栈,但具体怎么进行指令跳转的就需要用到条件跳转指令了,比较指令通常要和条件跳转指令配合使用。
跳转指令表示满足某条件后,跳转到指定位置。
条件跳转指令主要包括:ifeqifltifleifneifgtifgeifnullifnonnull
用法:条件跳转指令 + 操作数,操作数占2个字节,表示跳转的位置。
在这里插入图片描述

还是继续以8.1中的dcompTest为例,字节码指令如下所示

 0 dconst_1
 1 dstore_1
 2 ldc2_w #11 <2.0>
 5 dstore_3
 6 getstatic #2 <java/lang/System.out>
 9 dload_1
10 dload_3
11 dcmpg			#首先a变量的值1先压入栈,然后b的值2又压入栈,由于1<2,因此把-1压入栈,a和b的值分别出栈
12 ifge 19 (+7)		#ifge表示大于等于0满足跳转条件,由于栈顶元素为-1,不满足跳转条件,则继续按顺序向下执行即可;-1出栈
15 iconst_1			#由于没满足跳转条件,执行该行,把常整数1压入到操作数栈
16 goto 20 (+4)		#然后执行第2019 iconst_0
20 invokevirtual #6 <java/io/PrintStream.println>	#println重载了打印boolean类型,由于栈顶元素为1,则打印出true
23 return

8.3 比较条件跳转指令

8.1中介绍的是double、float、long类型的比较指令,比较之后的跳转由8.2节中的指令实现。那么两个int类型比较大小是如何实现?通过本节的比较条件跳转指令,比较条件跳转兼具了比较大小然后跳转的功能。
比较条件跳转指令主要包括如下
在这里插入图片描述

其中比较条件跳转是比较操作数栈最顶端的两个元素,前者元素为栈顶次位元素,后者为栈顶元素。

以如下java源码为例

public void ifComp(){
    int a = 5;
    int b = 10;
    System.out.println(a < b);
}

解析成JVM字节码指令如下所示

 0 iconst_5			#把5压入操作数栈
 1 istore_1			#把5存储到局部变量表索引1位置
 2 bipush 10 		#把10压入操作数栈
 4 istore_2			#把10存储到局部变量表索引2位置
 5 getstatic #3 <java/lang/System.out>
 8 iload_1			#把5加载到操作数栈
 9 iload_2			#把10加载到操作数栈
 9 iload_2
10 if_icmpge 17 (+7) #比较510的大小,如果5大于等于10,则跳转到第1713 iconst_1			#如果5小于10,就把常整数1压入操作数栈
14 goto 18 (+4) 	#跳转到第1817 iconst_0
18 invokevirtual #7 <java/io/PrintStream.println>
21 return

8.4 switch分支跳转指令

java源码中的switch分支跳转,对英语字节码指令分为:tableswitchlookupswitch
tableswitch:用于switch跳转中,case值连续的场景。它内部只存放起始值和终止值以及若干跳转偏移量,通过给定的偏移量进行跳转,效率高;
lookupswitch:存放各个离散的case值,case值都是不连续的,比如字符串,每次执行都要搜索全部的case值,匹配后进行跳转。

以如下java源码为例

public void tableSwitchTest(int value){
    int num;
    switch (value){
        case 1:
            num = 10;
            break;
        case 3:
            num = 30;
            break;
        case 2:
            num = 20;
            break;
        default:
            num = 40;
    }
}

解析成JVM指令如下所示

 0 iload_1			#局部变量表中索引为1位置的数据加载到操作数栈,即value的值
 1 tableswitch 1 to 3	1:  28 (+27) 	#case条件为13,按顺序来的,当case值为1时,跳转到第282:  40 (+39)					 	#case值为2时,跳转到第393:  34 (+33)						#case值为3时,跳转到第46default:  46 (+45)
28 bipush 10
30 istore_2
31 goto 49 (+18)
34 bipush 30
36 istore_2
37 goto 49 (+12)
40 bipush 20
42 istore_2
43 goto 49 (+6)
46 bipush 40
48 istore_2
49 return

从案例可以看出,case的值为1、2、3顺序的,采用tableswitch语句,案例中初始值为1,比如case=4时,直接通过4到1的偏移量即为3进行快速定位,效率高。
但是当case取下面的值时,case值不连续,就不能用tableswitch,字节码指令中采用的是lookupswitch指令。

public void tableSwitchTest(int value){
    int num;
    switch (value){
        case 1:
            num = 10;
            break;
        case 8:
            num = 30;
            break;
        case 2:
            num = 20;
            break;
        default:
            num = 40;
    }
}

案例二:
case值为整数时,如果连续用tableswitch,如果不连续用lookupswitch。如果case后面跟的是字符串呢?字节码指令采用的是loopupswitch,case的值是转化为了字符串的hash码进行比较的,如果hash值不同,认为不满足case条件,如果hash值满足case条件,然后再调用字符串的equal方法进行比较是否满足case条件的。
因为两个字符串进行比较,如果两个字符串的hash值不同,则这两个字符串一定不相等;如果这两个字符串的hash值相等,还要字符串的equal方法判断这两个字符串的内容是否相等。

    public String lookSwitchTest(String str){
        switch (str){
            case "aaa":
                return "aaa";
            case "bbb":
                return  "bbb";
            case "ccc":
                return "ccc";
        }
        return null;
    }

解析成JVM字节码指令如下

 0 aload_1				#把局部变量表中索引为1位置的数据加载到操作数栈,即str的值
 1 dup					#str的值在操作数栈中赋值一份
 2 astore_2				#str的值保持到局部变量表中索引为2的位置,并出栈
 3 invokevirtual #38 <java/lang/String.hashCode>	#对栈中的str执行hashCode方法,str的值出栈,得到的hashCode值入栈
 6 lookupswitch 3 		#从栈中取出str值的hashCode值,在下面3个条件中进行搜索
	96321:  40 (+34) 	#如果hashCode值为96321,就跳转到4097314:  52 (+46)
	98307:  64 (+58)
	default:  85 (+79)
40 aload_2				#把局部变量表中索引为2位置的数据加载到操作数栈,即str的值
41 ldc #44 <aaa> 		#把aaa字符串的地址加载到操作数栈
43 invokevirtual #46 <java/lang/String.equals> 	#通过字符串的equal方法判断str的值是否等于aaa,如果相等把1压入栈,反之把0压入栈
46 ifne 76 (+30) 		#如果栈顶元素不等于0,则跳转到7649 goto 85 (+36) 		#如果栈顶元素等于0,则跳转到8552 aload_2
53 ldc #50 <bbb>
55 invokevirtual #46 <java/lang/String.equals>
58 ifne 79 (+21)
61 goto 85 (+24)
64 aload_2
65 ldc #52 <ccc>
67 invokevirtual #46 <java/lang/String.equals>
70 ifne 82 (+12)
73 goto 85 (+12)
76 ldc #44 <aaa> 		#把aaa字符串的地址压入操作数栈
78 areturn 				#返回aaa的地址
79 ldc #50 <bbb>
81 areturn
82 ldc #52 <ccc>
84 areturn
85 aconst_null		#把null压入操作数栈
86 areturn 			#返回null

8.5 无条件跳转指令

主要用到的无条件跳转指令为goto,使用时goto加两个字节的操作数,共同组成一条完整命令。其中操作数用来指定跳转的偏移地址。前面很多案例已经用到了goto命令。
如果跳转的偏移量太大,超过双字节的带符号整数范围,则可以使用goto_w命令,goto_w命令可以接受4个字节的跳转偏移量。
除了上述两个无条件跳转指令,还有jsrjsr_wret用于无条件跳转指令,主要用于try-finally语句,已被虚拟机逐渐废弃。

9 异常处理

9.1 抛出异常指令

java中抛出异常throw功能都是由字节码指令athrow来实现的。执行异常指令后,会清除操作数栈上的所有内容。

案例一:以如下java源码为例

public void athrowTest(int i){
    if (i == 0){
        throw new RuntimeException("i的值为0");
    }
}

解析成JVM指令如下所示

 0 iload_1 				#把局部变量表索引位置1处数据加载到操作数栈,即i的值
 1 ifne 14 (+13) 		#如果i的值不等于0,就跳转到144 new #40 <java/lang/RuntimeException> 	#new 一个RuntimeException对象,并把对象地址压入操作数栈
 7 dup 					#复制RuntimeException对象的地址,压入操作数栈
 8 ldc #41 <i的值为0> 	#压入字符串“i的值为0”的地址到操作数栈
10 invokespecial #42 <java/lang/RuntimeException.<init>>	#消耗一个RuntimeException对象,调用构造器方法
13 athrow 				#抛出异常,栈中的RuntimeException对象地址和字符串“i的值为0”的地址出栈
14 return

案例二:对于JVM自动抛的异常,字节码中不会出现athrow命令,如下java源码所示,没有手动抛异常,JVM运行时会自动抛异常

public void athrowTest2(){
    int i = 1 / 0;
}

解析后的字节码如下所示

0 iconst_1
1 iconst_0
2 idiv
3 istore_1
4 return

9.2 异常处理表

java源码中处理异常用的try…catch或try…finally,对应的JVM字节码指令用的异常处理表形式。
异常表包含了每个异常处理或者finally代码块信息。异常表中每个异常处理信息包括:发生异常的起始位置、发生异常的终止位置、程序计数器记录的代码处理异常的偏移地址、被捕获的异常类在常量池中的索引。
当一个异常被抛出时,JVM会在异常表中查找匹配的异常处理方式,如果没找到就结束当前栈帧,异常抛给上一个调用的栈帧,如果在所有栈帧中都没有匹配异常的处理方式,线程将终止,如果异常在最后一个非守护线程里抛出,将会导致JVM终止,比如mian线程。
try代码块里面不管有无发生异常,如果有finally代码块,都会执行finally代码块。

以如下java源码为例

public void exceptionTest(){
    try {
        int i = 1 / 0;
        System.out.println("hello world");
    }catch (ArithmeticException ex){
        System.out.println("this is catch process");
    }
}

解析成JVM字节码指令如下

 0 iconst_1 		#把1压入操作数栈
 1 iconst_0			#把0压入操作数栈
 2 idiv				#计算1/0,并且10分别出栈,计算结果压入操作数栈
 3 istore_1 		#计算结果存储到局部变量表索引1位置,并出栈
 4 getstatic #3 <java/lang/System.out>	#获取out
 7 ldc #16 <hello world> 				#hello world字符串地址压入操作数栈,实际内容在字符串常量池中
 9 invokevirtual #43 <java/io/PrintStream.println> #调用println方法,打印出字符串
12 goto 24 (+12) 	#跳转到24行,结束执行,如果没有抛出异常,就此结束
15 astore_1 		#如果抛出异常了,异常对象地址会压入操作数栈,把异常对象地址存储到局部变量表索引为1的位置,并出栈
16 getstatic #3 <java/lang/System.out> #获取out
19 ldc #45 <this is catch process> 		#把字符串this is catch process的地址压入操作数栈
21 invokevirtual #43 <java/io/PrintStream.println> #调用println方法,打印出字符串
24 return

从上述字节码可以看出,如果try中代码块没有发生异常,即字节码中0~12行(不包括12行)没有发生异常,直接跳转到return结束方法执行。如果在发生了异常,从下面异常表中可以看到在0到12行字节码中发生ArithmeticException异常,就跳转到第15行进行处理。
异常表如下
在这里插入图片描述
案例二:

public String exceptionTest2(){ 	//最终返回的str结果为aaa
    String str;
    try {
        str = "aaa";
        return str;
    }finally {
        str = "bbb";
    }
}

解析成JVM字节码如下所示

 0 ldc #36 <aaa> 	#把字符串aaa的地址压入操作数栈
 2 astore_1 		#把aaa地址存储到局部变量表索引为1位置,并出栈
 3 aload_1 			#把aaa地址从局部变量表索引为1位置加载到操作数栈
 4 astore_2 		#把aaa地址存储到局部变量表索引为2位置,并出栈
 5 ldc #38 <bbb> 	#把字符串bbb的地址压入操作数栈
 7 astore_1 		#把字符串bbb的地址存储到局部变量表索引为1位置,并出栈
 8 aload_2 			#把局部变量表索引位置2处数据,即aaa地址加载到操作数栈
 9 areturn 			#返回
10 astore_3 			#如果0~5行发生任何异常异常,跳转到10行处理,并把异常压入操作数栈
11 ldc #38 <bbb>		#字符串bbb地址压入操作数栈
13 astore_1 			#bbb地址存储到局部变量表索引为1位置
14 aload_3 				#把异常对象地址加载到操作数栈
15 athrow 				#把栈顶的异常对象抛出来

从下面异常表中可以看出,在第0行到第5行之间发生了异常(任何类型的异常,因为捕获类型是any类型),就会跳转到第10行开始执行。
在这里插入图片描述

10 同步控制指令

java源码中采用synchronized进行同步控制时,字节码指令对应的是monitor监视器。同步结果分为两种:方法同步和方法内部代码块同步。

10.1 方法同步

方法级的同步,字节码监视器指令是隐式的,不会显示出现在字节码指令中,也无须通过字节码指令进行控制。这个同步锁或者监视器锁可以从方法区中方法常量池中对应的方法结构中的ACC_SYNCHRONIZED标识获取,如果有该标志则为同步方法。
当调用该方法时,调用指令会检查方法的ACC_SYNCHRONIZED标志是否设置。如果设置了,执行线程将先持有锁,然后执行方法,在方法执行完成后,不管正常执行完还是非正常执行完,都将释放锁。在方法执行期间,一个线程获得了锁,其它线程都无法获取。如果一个持有锁的线程在执行方法时抛出了异常,那么这个同步方法所持有的锁将在异常抛到同步方法外进行释放。

10.2 方法代码块同步

方法代码块同步可以由监视器指令进行控制,监视器指令由monitorentermonitorexit进行控制。
对于一段同步代码块,当一个线程执行到时,会先请求monitorentermonitorenter把监视器计数器从0设置为1,表示一个线程已经拥有了该代码块执行权,其它线程不会获取到,当该线程执行这一段同步代码块结束后,释放同步锁,请求monitorexit设置监视器计数器从1设置为0,此时其它线程可以获得执行权。

以如下java源码为例

Object obj = new Object();
int i = 0;
public void synchronizedTest(){
    synchronized (obj){
        i++;
    }
}

解析成JVM字节码指令如下

 0 aload_0 			#this对象加载到操作数栈中
 1 getfield #4 <com/lzj/classes/CodeDemo.obj> 	#通过当前对象中的obj引用对象,把obj对象地址压入操作数栈,并且this出栈
 4 dup 				#复制一份obj引用对象地址压入操作数栈
 5 astore_1			#此时栈中有2份obj对象地址,存储一份到局部变量表中索引为1的位置,并出栈一份
 6 monitorenter 	#进入监视器,拿到同步锁,obj出栈,拿锁其实是设置堆中obj对象头中锁标志置为1
 7 aload_0 			#this对象加载到操作数栈中
 8 dup 				#复制一份this对象到栈中,此时栈中有2this对象地址
 9 getfield #5 <com/lzj/classes/CodeDemo.i> 	#通过this对象获取i的值,this对象出栈一份,i值入栈
12 iconst_1 		#把1压入操作数栈
13 iadd				#把i加1的和压入操作数栈,i和1分别出栈
14 putfield #5 <com/lzj/classes/CodeDemo.i>	#把i+1的和赋值给i,i出栈
17 aload_1 			#obj加载到栈中
18 monitorexit		#去锁,obj出栈,设置堆中obj对象头中锁标志为0
19 goto 27 (+8) 	#跳转27行退出
22 astore_2 		#查看异常表中,如果719之间抛异常,就跳转到22行,把异常对象地址压入操作数栈
23 aload_1 			#obj对象地址加载到操作数栈
24 monitorexit 		#去锁,抛异常后,要把锁去掉,方便后续线程获取锁
25 aload_2 			#把异常对象地址加载到操作数栈
26 athrow 			#抛异常
27 return 			#结束返回

从字节码指令中可以看出,第6行为上锁操作,第18行为解锁操作,中间内容就位同步的字节码指令执行,从异常表中看可以看出有2条记录,第一条记录是从第7行(也就是加锁操作第6行的下一步)到第19行(解锁18行后的下一步)之间发生了异常,不管什么类型的异常(因为捕获类型是any类型的,任何异常),都将跳转到第22行执行,进行解锁操作,就是说即使抛异常了,也应该释放锁后再抛异常。但是从异常表中第二条记录可以看出,当第22行第25行之间发生异常了(任何类型的异常),重新跳转到第22行执行,也就是说如果释放锁失败了,继续重新释放锁,如果一直释放锁失败就会一直重复开始从第22行执行。
在这里插入图片描述


版权声明:本文为u010502101原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>