一个字节长度
的、代表着某种特定操作含义的数字(称为操作码,Opcode
)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands
)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码。对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务
也有一些指令的助记符中没有明确地指明操作类型的字母,如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类型作为运算类型(Computational Type)。
作用
加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递。
常用指令
上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload)。这些指令助记符实际上代表了一组指令(例如iload代表了iload0、iload1、iload2和iload3这几个指令)。这几组指令都是某个带有一个操作数的通用指令(例如iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中。
例如:
iload_0:将局部变量表中索引为0位置上的数据压入操作数栈中。 iload 0:将局部变量表中索引为0位置上的数据压入操作数栈中。 iload 4:将局部变量表中索引为4位置上的数据压入操作数栈中。
前两种所表达的意思是相同的,不过iload_0相当于是只有操作码所以只占用1个字节,而iload 0 是操作码和操作数所组成的,而操作数占 2 个字节,所以占用3个字节。
默认最多只有0-3。
这类指令大体可以分为:
代码示例:
// 1 局部变量入栈命令 public void load(int num,Object obj,long count,boolean flag,short[] arr){ System.out.println(num); System.out.println(obj); System.out.println(count); System.out.println(flag); System.out.println(arr); }
所对应的局部变量表
常量入栈指令的功能是将常数压入操作数栈,根据数据类型和入栈内容的不同,又可以分为const系列、push系列和ldc指令。
指令const系列:用于对特定的常量入栈,入栈的常量隐含在指令本身里。指令有:iconst_<i>
(i从-1到5)、lconst_<1>
(1从0到1)、fconst_<f>
(f从0到2)、dconst_<d>
(d从0到1)、aconst_null
。
比如:
const_x,是变量值,并且是有范围,比如int大于5,就要使用push系列
指令push系列:主要包括bipush和sipush。它们的区别在于接收数据类型的不同,bipush接收8位整数作为参数,sipush接收16位整数,它们都将参数压入栈。
指令ldc系列:如果以上指令都不能满足需求,那么可以使用万能的ldc指令,它可以接收一个8位的参数,该参数指向常量池中的int、float或者String的索引,将指定的内容压入堆栈。
代码示例:
public void pushConstLdc() { int i = -1; int a = 5; int b = 6; int c = 127; int d = 128; int e = 32767; int f = 32768; }
所对应的字节码指令如下:
指令总结如下:
类型 | 指令 | 范围 |
---|---|---|
int(boolean,byte,char,short) | iconst | [-1,5] |
bipush | [-128,127] | |
sipush | [-32768,32767] | |
ldc | any int value | |
long | lconst | 0,1 |
ldc | any int value | |
float | fconst | 0,1,2 |
ldc | any int value | |
double | dconst | 0,1 |
ldc | any int value | |
reference | aconst | null |
ldc | String literal,Class literal |
出栈装入局部变量表指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值。
这类指令主要以store的形式存在,比如xstore(x为i、l、f、d、a)、xstore_n(x为i、l、f、d、a,n为0至3)。
说明:
一般说来,类似像store这样的命令需要带一个参数,用来指明将弹出的元素放在局部变量表的第几个位置
。但是,为了尽可能压缩指令大小,使用专门的istore_1指令表示将弹出的元素放置在局部变量表第1个位置。类似的还有istore_0、istore_2、istore_3,它们分别表示从操作数栈顶弹出一个元素,存放在局部变量表第0、2、3个位置。因此这种做法虽然增加了指令数量,但是可以大大压缩生成的字节码的体积
。如果局部变量表很大,需要存储的槽位大于3,那么可以使用istore指令,外加一个参数,用来表示需要存放的槽位位置。代码示例:
public void store(int k, double d) { int m = k + 2; long l = 2; String str = "jack"; float f = 10.0F; d = 10; }
对应的字节码指令和局部变量表:
作用
算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新压入操作数栈。
分类
大体上算术指令可以分为两种:对整型数据
进行运算的指令和对浮点类型数据
进行运算的指令。
byte、short、char和boolean类型说明
在每一大类中,都有针对Java虚拟机具体数据类型的专用算术指令。但没有直接支持byte、short、char和boolean类型的算术指令,对于这些数据的运算,都使用int类型的指令来处理。此外,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。
运算时的溢出
数据运算可能会导致溢出,例如两个很大的正整数相加,结果可能是一个负数。其实Java虚拟机规范并无明确规定过整型数据溢出的具体结果,仅规定了在处理整型数据时,只有除法指令以及求余指令中当出现除数为0时会导致虚拟机抛出异常ArithmeticException。
运算模式
NaN值使用
当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义的话,将会使用NaN值来表示。而且所有使用NaN值作为操作数的算术操作,结果都会返回NaN;(Infinity无穷大)
public void test() { int i = 10; double j = i / 0.0; System.out.println(j); // Infinity double d1 = 0.0; double d2 = d1 / 0.0; System.out.println(d2); // NaN }
所有算术指令
位运算指令:
代码示例:
public void method2() { float i = 10; float j = -i; i = -j; }
对应字节码如下:
比较指令有:dcmpg,dcmpl、fcmpg、fcmpl、lcmp。
Java虚拟机直支持以下数值的宽化类型转换(widening numeric conversion,小范围类型向大范围类型的安全转换)。也就是说,并不需要指令执行,包括:
简化为:int–>long–>float–>double
// 宽化类型转换 public void test() { int i = 10; long l = i; // i2l float f = i; // i2f double d = i; // i2d float f1 = l; // l2f double d1 = l; // l2d double d2 = f1; // f2d }
精度损失问题
代码示例:
public void upCast2() { int i = 123123123; float f = i; System.out.println(f); // 1.2312312E8 = 123123120 精度丢失 long l = 123123123123123123L; //1.2312312312312312E17 double d = l; //123123123123123120 精度丢失 System.out.println(d); }
补充说明
从byte、char和short类型到int类型的宽化类型转换实际上是不存在的。对于byte类型转为int,虚拟机并没有做实质性的转化处理,只是简单地通过操作数栈交换了两个数据。而将byte转为long时,使用的是i2l,可以看到在内部byte在这里已经等同于int类型处理,类似的还有short类型,这种处理方式有两个特点:
Java虚拟机也直接支持以下窄化类型转换:
代码示例:
public void downCastl() { int i = 10; byte b = (byte) i; // i2b short s = (short) i; // i2s char c = (char) i; // i2c long l = 10L; int il = (int) l; // l2i byte b1 = (byte) l; // l2i i2b } public void downCast2() { float f = 10; long l = (long) f; // f2l int i = (int) f; // f2i byte b = (byte) f; // f2i i2b double d = 10; byte b1 = (byte) d; // d2i i2b }
精度损失问题
补充说明
当将一个浮点值窄化转换为整数类型T(T限于int或long类型之一)的时候,将遵循以下转换规则:
当将一个double类型窄化转换为float类型时,将遵循以下转换规则:通过向最接近数舍入模式舍入一个可以使用float类型表示的数字。最后结果根据下面这3条规则判断:
Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支。有一系列指令专门用于对象操作,可进一步细分为创建指令、字段访问指令、数组操作指令、类型检查指令。
创建类实例的指令:
创建类实例的指令:new
创建数组的指令:
创建数组的指令:newarray、anewarray、multianewarray。
上述创建指令可以用于创建对象或者数组,由于对象和数组在Java中的广泛使用,这些指令的使用频率也非常高。
代码示例:
// 创建对象 public void newInstance() { Object obj = new Object(); File file = new File("Hello.txt"); }
对应的字节码如下:
dup是将栈顶数值复制一份并送入至栈顶。因为invokespecial会消耗掉一个当前类的引用,因而需要复制一份。
// 创建数组 public void newArray() { int[] intArray = new int[10]; // newarray Object[] objArray = new Object[10]; // anewarray int[][] mintArray = new int[10][10]; // multianewarray String[][] strArray = new String[10][]; // newarray String[][] strArray2 = new String[10][5]; // multianewarray }
对象创建后,就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素。
数组操作指令主要有:xastore和xaload指令。
具体为:
取数组长度的指令:arraylength
数组类型 | 加载指令 | 存储指令 |
---|---|---|
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 |
虚拟机栈中并不存储数组元素信息,所以astore改变的是堆中的实例数组
说明
代码示例:
public void text() { int[] intArray = new int[10]; intArray[3] = 20; System.out.println(intArray[1]); }
对应的字节码如下:
检查类实例或数组类型的指令:instanceof、checkcast。
//类型检查指令 public String checkcast(Object obj) { if (obj instanceof String) { return (String) obj; } else { return null; } }
对应的字节码如下:
方法调用指令:invokevirtual、invokeinterface、invokespecial、invokestatic、invokedynamic以下5条指令用于方法调用:
的类方法(static方法)
。这是静态绑定的。invokespecial 和invokestatic 都不可能重写
代码示例:
public void invoke() { //情况1:类实例构造器方法:<init>() Date date = new Date(); Thread t1 = new Thread(); //情况2:父类的方法 super.toString(); //情况3:私有方法 methodPrivate(); } private void methodPrivate() {}
对应的字节码如下:
//方法调用指令:invokestatic public void invoke() { methodstatic(); //0 invokestatic #2 <com/test/Demo.methodstatic> //3 return } private static void methodstatic() {}
//方法调用指令:invokeinterface public void invoke() { Thread t1 = new Thread(); ((Runnable) t1).run(); Comparable<Integer> com = null; com.compareTo(123); }
对应的字节码如下:
方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的。
返回类型 | 返回指令 |
---|---|
void | return |
int (boolean,byte,char,short) | ireturn |
long | lreturn |
float | freturn |
double | dreturn |
reference | areturn |
注意:
代码示例:
public float returnFloat() { int i = 10; return i; }
对应的字节码如下:
这类指令包括如下内容:
这些指令属于通用型,对栈的压入或者弹出无需指明数据类型。
说明:
不带x的指令是复制栈顶数据并压入栈顶。包括两个指令,dup和dup2。dup的系数代表要复制的Slot个数。
dup_×1,dup2_×1,dup_×2,dup2_×2
对于带_x的复制插入指令,只要将指令的dup和x的系数相加,结果即为需要插入的位置。因此
程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为:
比较指令有:dcmpg,dcmpl、fcmpg、fcmpl、lcmp。
指令 | 说明 |
---|---|
ifeq | 当栈顶int类型数值等于0时跳转 |
ifne | 当栈顶int类型数值不等于0时跳转 |
iflt | 当栈顶int类型数值小于0时跳转 |
ifle | 当栈顶int类型数值小于等于0时跳转 |
ifgt | 当栈顶int类型数值大于0时跳转 |
ifge | 当栈顶int类型数值大于等于0时跳转 |
ifnull | 为null时跳转 |
ifnonnull | 不为null时跳转 |
注意:
与前面运算规则一致:
代码示例:
public void compare() { int a = 0; if (a == 0) { a = 10; } else { a = 20; } }
比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。
这类指令有:
其中指令助记符加上“if_”后,以字符“i”开头的指令针对int型整数操作(也包括short和byte类型),以字符“a”开头的指令表示对象引用的比较。
指令 | 说明 |
---|---|
if_icmpeq | 比较栈顶两个int类型数值大小,当前者等于后者时跳转 |
if_icmpne | 比较栈顶两个int类型数值大小,当前者不等于后者时跳转 |
if_icmplt | 比较栈顶两个int类型数值大小,当前者小于后者时跳转 |
if_icmple | 比较栈顶两个int类型数值大小,当前者小于等于后者时跳转 |
if_icmpgt | 比较栈顶两个int类型数值大小,当前者大于后者时跳转 |
if_icmpge | 比较栈顶两个int类型数值大小,当前者大于等于后者时跳转 |
if_acmpeq | 比较栈顶两个引用类型数值,当结果相等时跳转 |
if_acmpne | 比较栈顶两个引用类型数值,当结果不相等时跳转 |
代码示例:
public void compare() { int i = 10; int j = 20; System.out.println(i > j); }
public void compare() { Object obj1 = new Object(); Object obj2 = new Object(); // new 指向堆内存地址不一样 System.out.println(obj1 == obj2); //false System.out.println(obj1 != obj2);//true }
多条件分支跳转指令是专为switch-case语句设计的,主要有tableswitch和lookupswitch。
从助记符上看,两者都是switch语句的实现,它们的区别:
指令tableswitch的示意图如下图所示。由于tableswitch的case值是连续的,因此只需要记录最低值和最高值,以及每一项对应的offset偏移量,根据给定的index值通过简单的计算即可直接定位到offset。
指令lookupswitch处理的是离散的case值,但是出于效率考虑,将case-offset对按照case值大小排序,给定index时,需要查找与index相等的case,获得其offset,如果找不到则跳转到default。指令lookupswitch如下图所示。
代码示例:
public void switchTest(int select) { int num; switch (select) { case 1: num = 10; break; case 2: num = 20; // break; case 3: num = 30; break; default: num = 40; } }
public void switchTest(int select) { int num; switch (select) { case 100: num = 10; break; case 500: num = 20; break; case 200: num = 30; break; default: num = 40; } }
public void whileInt() { int i = 0; while (i < 100) { String s = "Jack"; i++; } }
athrow指令
注意:
正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时,Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。
代码示例:
public void throwZero(int i) { if (i == 0) { throw new RuntimeException("参数错误"); } }
处理异常
异常表
当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。
不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标
代码示例:
public void tryCatch() { try { File file = new File("hello.txt"); FileInputStream fis = new FileInputStream(file); String info = "hello!"; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (RuntimeException e) { e.printStackTrace(); } }
java虚拟机支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的。
当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置。
public synchronized void test() {}
代码示例:
private int i = 0; private Object obj = new Object(); public void subtract() { synchronized (obj) { i--; } }