目录
一、概述
算术指令用于对两个操作数栈上的值进行某种特定运算,并把计算之后的结果重新压入操作数栈中。
【a】算术指令大体上可以分为两种:
- 对整型数据进行运算的指令;
- 对浮点类型数据进行运算的指令;
【b】byte、short、char和boolean类型说明
在每一大类中,都有针对Java虚拟机具体数据类型的专用算术指令。但没有直接支持byte、 short、 char和boolean类型的算术指令,对于这些数据的运算,都使用int类型的指令来处理。此外,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。
【c】Java虚拟机中的实际类型与运算类型
实际类型 | 运算类型 |
boolean | int |
byte | int |
char | int |
short | int |
int | int |
float | float |
reference | reference |
returnAddress | returnAddress |
long | long |
double | double |
数据运算可能会导致溢出,例如两个很大的正整数相加,结果可能是一个负数。其实Java虚拟机规范并无明确规定过整型数据溢出的具体结果,仅规定了在处理整型数据时,只有除法指令以及求余指令中当出现除数为0时会导致虚拟机抛出算术运算异常ArithmeticException。
【d】运算模式
向最接近数舍入模式:JVM要求在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的;
向零舍入模式:将浮点数转换为整数时,采用该模式,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果;
【e】NaN值使用
当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义的话,将会使用 NaN值来表示。而且所有使用NaN值作为操作数的算术操作,结果都会返回 NaN;
public void method1(){
int i = 10;
double j = i / 0.0;
System.out.println(j);//无穷大
double d1 = 0.0;
double d2 = d1 / 0.0;
System.out.println(d2);//NaN: not a number
}
二、所有算术指令
所有的算术指令包括:
加法指令:iadd、ladd、fadd、dadd
减法指令:isub、lsub、fsub、dsub
乘法指令:imul、lmul、fmul、dmul
除法指令:idiv、ldiv、fdiv、ddiv
求余指令:irem、lrem、frem、drem //remainder:余数
取反指令:ineg、lneg、fneg、dneg //negation:取反
自增指令:iinc
位运算指令,又可分为:
- 位移指令:ishl、ishr、 iushr、lshl、lshr、 lushr
- 按位或指令:ior、lor
- 按位与指令:iand、land
- 按位异或指令:ixor、lxor
比较指令: dcmpg、dcmpl、 fcmpg、fcmpl、lcmp
下面我们看一个示例:
public void method2() {
float i = 10;
float j = -i;
i = -j;
}
查看其字节码解析即分析如下:
0 ldc #21 <10.0> //将10压入操作数栈中
2 fstore_1 //将10存储在局部变量中索引为1的位置
3 fload_1 //将局部变量表中索引为1的值10重新压入操作数栈中
4 fneg //对i取反
5 fstore_2 //将i取反的结果j存储在局部变量表中索引为2的位置
6 fload_2 //将局部变量表中索引为2的j变量的值重新压入操作数栈中
7 fneg //对j取反
8 fstore_1 //将取反后的结果存入局部变量表中索引为1的位置
9 return //方法返回
自增与相加运算示例一
public void method3(int j) {
int i = 100;
i = i + 10;
}
查看其字节码解析即分析如下:
0 bipush 100 //将100压入操作数栈中
2 istore_2 //将100存入局部变量表中索引为2的位置
3 iload_2 //局部变量表中索引为2的值100压入操作数栈中
4 bipush 10 //将10压入操作数栈中
6 iadd //将操作数栈顶两个元素做相加操作,然后将计算后的110存入操作数栈中
7 istore_2 //将110存入局部变量表中索引为2的位置
8 return //方法返回
图解分析如下:
自增与相加运算示例二
public void method4(int j) {
int i = 100;
i += 10;
}
查看其字节码解析即分析如下:
0 bipush 100 //将100压入操作数栈中
2 istore_2 //将100存入局部变量表中索引为2的位置
3 iinc 2 by 10 //将局部变量表中的值加上10,变成110
6 return //方法返回
图解分析如下:
异或运算举例
public int method5(int i, int j) {
return ((i + j - 1) & ~(j - 1));
}
查看其字节码解析即分析如下:
0 iload_1 //局部变量表中索引为1的值压入操作数栈中
1 iload_2 //局部变量表中索引为2的值压入操作数栈中
2 iadd //将操作数栈顶两个元素做相加操作,计算后的结果重新压入操作数栈中
3 iconst_1 //将常量1压入操作数栈中
4 isub //做一个减法操作,计算后的结果重新压入操作数栈中
5 iload_2 //局部变量表中索引为2的值压入操作数栈中
6 iconst_1 //将常量1压入操作数栈中
7 isub //做一个减法操作,计算后的结果重新压入操作数栈中
8 iconst_m1 //将常量-1压入操作数栈中
9 ixor //做一个异或操作
10 iand //做一个与运算
11 ireturn //方法返回
静态方法操作数栈中没有this:方法入参i占据了第0个索引
public static int method6(int i) {
return ((i + 1) - 2) * 3 / 4;
}
假设调用method6(5)进行计算,那么此时字节码指令相关操作解析如下:
0 iload_0 //将局部变量表中索引为0位置上的值5压入操作数栈中
1 iconst_1 //将常量1压入操作数栈中
2 iadd //将栈顶两个元素做一个相加操作,然后计算之后的值6重新压入操作数栈中
3 iconst_2 //将常量2压入操作数栈中
4 isub //做一个减法操作,然后【6-2】= 4重新压入操作数栈中
5 iconst_3 //将常量3压入操作数栈中
6 imul //做一个乘法操作,4 * 3 = 12,然后12存入操作数栈中
7 iconst_4 //将常量4压入操作数栈中
8 idiv //做一个除法操作,12 /4 = 3,然后3存入操作数栈中
9 ireturn //方法返回
图解分析如下:
查看jclasslib对应的局部变量表如下图所示:
三、彻底搞懂i++与++i的问题
直接看下面两个程序:
public void method6(){
int i = 10;
i++;
}
public void method7(){
int i = 10;
++i;
}
我们编译之后,发现上述两个程序的字节码信息是一样的,如下:
0 bipush 10 //将10压入操作数栈中
2 istore_1 //将10存到局部变量表中索引为1的位置
3 iinc 1 by 1 //将局部变量表中索引为1的值10,加1,变成11
6 return //方法返回
我们可以看到,如果不涉及赋值操作,从字节码角度看i++和++i是一样的。
但是涉及到赋值操作时,字节码就有所区别了:
public void method7(){
int i = 10;
int a = i++;
int j = 20;
int b = ++j;
}
字节码信息解析如下:
0 bipush 10 //将10压入操作数栈中
2 istore_1 //将10存入局部变量表中索引为1的位置【变量i】
3 iload_1 //重新将局部变量表中索引为1的值10压入操作数栈中
4 iinc 1 by 1 //将局部变量表中索引为1的值10做一个加1操作,变成11
7 istore_2 //将10存入局部变量表中索引为2的位置【变量a】,所以a = 10
8 bipush 20 //将20压入操作数栈中
10 istore_3 //将20存入局部变量表中索引为3的位置【变量j】
11 iinc 3 by 1 //将局部变量表中索引为3的值20做一个加1操作,变成21
14 iload_3 //将局部变量表中索引为3的值21压入操作数栈中
15 istore 4 //将21存入局部变量表中索引为4的位置【变量b】,所以b = 21
17 return //方法返回
结合图解分析一下:
如上,我们可以总结出:
- i++是先赋值后运算;
- ++i是先运算后赋值;
四、比较指令的说明
- 比较指令的作用是比较栈顶两个元素的大小,并将比较结果压入操作数栈中;
- 比较指令有:dcmpg, dcmpl、fcmpg、fcmpl、lcmp;
-
- 与前面讲解的指令类似,首字符d表示double类型,f表示float,l表示long;
- 对于double和float类型的数字,由于NaN的存在,各有两个版本的比较指令。以float为例,有fcmpg和fcmpl两个指令,它们的区别在于在数字比较时,若遇到NaN值,处理结果不同;
- 指令lcmp针对long型整数,由于long型整数没有NaN值,故无需准备两套指令;
举例:
指令fcmpg和fcmpl都从栈中弹出两个操作数,并将它们做比较,设栈顶的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入0;若v1 > v2则压入1;若v1 < v2则压入 -1。
两个指令的不同之处在于,如果遇到NaN值,fcmpg会压入1,而fcmpl会压入-1。