操作码/Opcode
)以及跟随其后的零至多个代表此操作所需参数(称为 操作数/Oprands
)而构成。由于 Java 虚拟机采用面向操作数栈而不是寄存器的结构。所以大多数指令都不包含操作数,只有一个操作码。熟悉虚拟机指令对于动态字节码生成、反编译Class文件、Class文件修补都有着非常重要的价值。因此,阅读字节码作为了解Java虚拟机的基础技能,需要熟练掌握常见指令。
官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html
如果不考虑异常处理,那么Java虚拟机的解释器可以使用下面这个伪代码当作最基本的执行模型来理解。
do{
自动计算PC寄存器的值+1;
根据PC寄存器的指示位置,从字节码流中取出操作码;
if(字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
}while(字节码长度>0)
在Java虚拟机的指令集中,大多数的指令都包含了其操作数所对应的数据类型信息。例如,iload
指令用于从局部变量表中加载int型的数据到操作数栈中,而 fload
指令加载的则是float类型的数据。
对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务。
也有一些指令的助记符中 没有明确地指明操作类型的字母,如 arraylength
指令,它没有代表数据类型的特殊字符,但操作数永远也只能是一个数组类型的对象。
还有另外一些指令,如无条件跳转指令 goto
则是与 数据类型无关的。
大部分的指令都没有支持整数类型byte、char、short。甚至没有任何指令支持 boolean 类型。编译器会在 编译期/运行期 将 byte 和 short 类型的数据 带符号扩展(Sing-Extend)为相应的int类型数据,将 boolean 和 char 类型数据 零位扩展(Zero-Extend)为相应的int类型数据。与之类似,在处理 boolean、byte、short、char 类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于boolean、byte、short、char类型数据的操作。
为尽快熟悉这些基本指令,将JVM中的字节码指令集按用途分为9类。
在做值相关操作时:
作用:
加载和存储指令用于将数据从栈帧的局部变量表和操作数之间来回传递。
常用指令:
xload
、xload_
(其中x为i、l、f、d、a,而n为0~3)bipush
、sipush
、ldc
、ldc_w
、ldc2_w
、aconst_null
、iconst_m1
、iconst_
、lconst_
、fconst_
、dconst_
xstore
、xstore_
其中x为i、l、f、d、a,而n为0~3);xastore
(其中x为i、l、f、d、a、b、c、s)wide
上面列举的指令助记符中,有一部分是以尖括号结尾的(例如iload_
)。这些指令助记符实际上代表了一组指令(例如iload_
代表了iloadd_0
、iload_1
、iload_2
、iload_3
这几个指令)。这几组指令都是某个带有一个操作数的通用指令(例如iload
)的特殊形式。对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行去操作数的动作,但操作数都隐含在指令中。
iload_0 //将局部变量表中索引为0位置上的数据压入操作数栈中
iload 0 //将局部变量表中索引为0位置上的数据压入操作数栈中
iload 4 //将局部变量表中索引为4位置上的数据压入操作数栈中
除此之外,它们的语义与原生的通用指令完全一致(例如 iload_0
的语义与 操作数0 时的 iload
指令完全一致)。在尖括号之间的字母制定了指令隐含操作数的数据类型,
操作 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
指针(仅非静态方法)、所传入的参数、字节码中的局部变量。
与操作数栈一样,long 类型、double 类型的值将占据两个单元(Slot 槽),其余类型仅占据一个单元。
举例:
public void foo(long l, float f) {
{
int i = 0;
}
{
String s = "Hello, World!";
}
/* i/s:槽位复用 */
}
在栈桢中,与性能调优关系最为密切的部分就是局部变量表。局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接/间接引用的对象都不会被回收。
在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表压栈指令将给定的局部变量表中的数据压入操作数栈。
这类指令大体可以分为:
xload
(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
等。
常量入栈指令的功能:将常数压入操作数栈,根据数据类型和入栈内容不同,又可以分为 const系列、push系列、ldc系列。
用于对特定的常量入栈,入栈的常量隐含在指令本身里。无法附加操作数,无 xconst n 指令。
指令有:iconst_
(i从-1到5)、lconst_
(l从0到1)、fconst_
(f从0到2)、dconst_
(d从0到1)、aconst_null
。
例如:
iconst_m1 将-1压入操作数栈
iconst_x(x∈{0,1,2,3,4,5}) 将x压入栈
iconst_0、iconst_1 分别将长整数0和1压入栈
fconst_0、fconst_1、fconst_2 分别将浮点数0、1、2压入栈
dconst_0、dconst_1 分别将double型的0和1压入栈
aconst_null 将null压入操作数栈
从指令的命名上可找到规律,指令助记符的第一个字符总是喜欢表示数据类型,i表示整数,l表示长整数,f表示浮点数,d表示双精度浮点数,习惯上用a表示对象引用。如果指令隐含操作的参数,会以下划线形式给出。
//本身无法附加操作数,没有对应指令
int i = 3; iconst_3
int j = 6; bipush 6
int k = 32768; ldc 索引
主要包括 bipush
和 sipush
。它们的区别在于接收数据类型的不同,bipush
接收8位整数作为参数,sipush
接收16位整数,它们都将参数压入栈。
如果以上指令都不能满足要求,那么可以使用万能的 ldc
指令,它可以接收一个8位的参数,该参数指向常量池中的 int、float、String 的索引,将指定的内容压入栈中。
类似的还有 ldc_w
,它接收两个8位参数,能支持的索引范围大于ldc
。
如果要压入的元素是 long 或 double 类型的,则使用 ldc2_w
指令,使用方式都是类似的。
类型 | 常数指令 | 范围 |
---|---|---|
int(boolean, byte, char, short) | iconst_ | [-1 ,5] |
bipush | [-128, 127] | |
sipush | [-32768, 32767] | |
ldc | any int value | |
long | lconst_ |
0, 1 |
ldc | any long value | |
float | fconst_ |
0, 1, 2 |
ldc | any float value | |
double | dconst_ |
0, 1 |
ldc | any double value | |
reference | aconst_null | null |
ldc | String literal, Class literal, |
举例说明:
Java源代码:
// 常量入栈指令 - int
public void pushConstLdc() {
int i = -1; // iconst_m1
int a = 5; // iconst_5
int b = 6; // bipush 6
int c = 127; // bipush 127
int d = 128; // sipush 128
int e = 32767; // sipush 32767
int f = 32768; // ldc #7 <32768>
}
JVM字节码:
0 iconst_m1
1 istore_1
2 iconst_5
3 istore_2
4 bipush 6
6 istore_3
7 bipush 127
9 istore 4
11 sipush 128
14 istore 5
16 sipush 32767
19 istore 6
21 ldc #7 <32768>
23 istore 7
25 return
Java 源代码:
// 常量入栈指令 - 其他类型
public void constLdc() {
long a1 = 1; // lconst_1
long a2 = 2; // ldc2_w #8 <2>
float b1 = 2; // fconst_2
float b2 = 3; // ldc #10 <3.0>
double c1 = 1; // dconst_1
double c2 = 2; // ldc2_w #11 <2.0>
Date d = null; // aconst_null
}
JVM字节码:
0 lconst_1
1 lstore_1
2 ldc2_w #8 <2>
5 lstore_3
6 fconst_2
7 fstore 5
9 ldc #10 <3.0>
11 fstore 6
13 dconst_1
14 dstore 7
16 ldc2_w #11 <2.0>
19 dstore 9
21 aconst_null
22 astore 11
24 return
出栈装入局部变量表指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值。
这类指令主要以 store 的形式存在,比如xstore
(x为i、l、f、d、a)、xstore_n
(x为i、l、f、d、a,而n为0~3)、xastore
(x为i、l、f、d、a、b、c、s)
istore_n
将从操作数栈中谭舒一个整数,并把它赋值给局部变量n。xstore
由于没有隐含参数信息,故需要提供一个 byte 类型的参数类指定目标局部变量表的位置。xastore
则专门针对数组操作,以iastore
为例,它用于给一个int数组的给定索引赋值。在iastore
执行前,操作数栈顶需要以此准备3个元素:值、索引、数组引用,iastore
会弹出这3个值,并将值赋给数组中指定索引的位置。说明:
一般来说,类似像 store 这样的命令需要一个参数,用来指明将弹出的元素放在局部变量表的第几个位置。但是,为了尽可能压缩指令大小,使用专门的 istore_1
指令表示将弹出的元素放置在局部变量表的第1个位置。类似的还有 istore_0
、istore_2
、istore_3
,它们分别表示从操作数栈顶弹出一个元素,存放在局部变量表的第0、2、3个位置。
由于局部变量表前几个位置总是非常常用 ,因此 这种做法虽然增加了指令数量,但是可以大大压缩生成的字节码的体积。如果局部变量表很大,需要存储的槽位>3,那么可以使用 istore
指令,外加一个参数,用来表示需要存放的槽位位置。
作用:
算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新压入操作数栈。
分类:
大体上算数指令可以分为两种:对 整型数据 进行运算的指令与对 浮点类型数据 进行运算的指令。
byte、short、char、boolean 类型说明:
在每一大类中,都有针对Java虚拟机具体类型的专用算术指令。但没有直接支持 byte、short、char、boolean 类型的算术指令,对于这些数据的运算,都使用 int 类型的指令来处理。此外,在处理 boolean、byte、short、char 类型的数组时,也会转化为对应的int类型的字节码指令来处理。
运算时的溢出:
数据运算可能会导致溢出,例如两个很大的正整数相加,结果可能是一个负数。其实Java虚拟机规范并无明确规定过整型数据溢出的具体结果,仅规定了在处理整型数据时,只有除法指令以及求余指令中当出现除数为0时,会导致虚拟机抛出异常 ArithmeticException
。
运算模式:
NaN值的使用:
当一个操作数产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义,将会使用 NaN值 来表示。而且所有使用NaN值 (not a number) 作为操作数的算术操作,结果都会返回 NaN。
所有的算术指令包括:
加法指令: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
举例1:
int i =10;
/* 两种情况 */
i = i + 10; //加法 : iadd
i += 10; //自增 : iinc 2 by 10
举例2:
Java源码:
public int method(int i, int j) {
return ((i + j - 1) & ~(j - 1));
}
JVM字节码:
0 iload_1
1 iload_2
2 iadd
3 iconst_1
4 isub
5 iload_2
6 iconst_1
7 isub
8 iconst_m1 //取反~ 步骤1:取-1
9 ixor //取反~ 步骤2:异或
10 iand
11 ireturn
Java源代码:
public static int bar(int i) {
return ((i + 1) - 2) * 3 / 4;
}
本地变量表 + 操作数栈 图示:
Java源代码:
public void add() {
byte i =15;
int j = 8;
int k = i + j;
}
图示(局部变量表0位置为this → 已省略):
Java源代码:
public static void main(String[] args) {
int x = 500;
int y = 100;
int a = x / y;
int b = 50;
System.out.println(a + b);
}
图示:
例1:
Java源代码:
public void methodA1() {
int i = 10;
i++;
}
public void methodB1() {
int i = 10;
++i;
}
JVM字节码(两方法一致):
0 bipush 10
2 istore_1
3 iinc 1 by 1
6 return
例2:
Java源代码:
public void method2() {
int i = 10; // i:10
int a = i++; // a:11
int j = 20; // j:20 --> 21
int b = ++j; // b:21
}
/**
* i:10
* a:11
* j:21
* b:21
*/
JVM字节码:
0 bipush 10
2 istore_1
3 iload_1
4 iinc 1 by 1
7 istore_2
8 bipush 20
10 istore_3
11 iinc 3 by 1
14 iload_3
15 istore 4
17 return
例3:
Java源代码:
public void method3() {
int i = 10;
i = i++;
System.out.println(i); // i:10
}
JVM字节码:
0 bipush 10
2 istore_1
3 iload_1
4 iinc 1 by 1
7 istore_1
8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return
例4-1:
Java源代码:
public static void methodA4() {
int i = 10;
i = ++i + i++;
System.out.println(i); //i:22
}
JVM字节码:
0 bipush 10 // 栈:BOTTOM→TOP
2 istore_1 // 表:10; 栈:
3 iinc 1 by 1 // 表:11; 栈: //++i
6 iload_1 // 表:11; 栈:11 //++i
7 iload_1 // 表:11; 栈:11 11 //i++
8 iinc 1 by 1 // 表:12; 栈:11 11 //i++
11 iadd // 表:12; 栈:22
12 istore_1 // 表:22; 栈:
13 getstatic #2 <java/lang/System.out>
16 iload_1
17 invokevirtual #3 <java/io/PrintStream.println>
20 return
例4-2:
Java源代码:
public void methodB4() {
int i = 10;
i = i++ + ++i;
System.out.println(i); //i:22
}
JVM字节码:
0 bipush 10 // 栈:BOTTOM→TOP
2 istore_1 // 表:10; 栈:
3 iload_1 // 表:10; 栈:10
4 iinc 1 by 1 // 表:11; 栈:10
7 iinc 1 by 1 // 表:12; 栈:
10 iload_1 // 表:12; 栈:10 12
11 iadd // 表:12; 栈:22
12 istore_1 // 表:22; 栈:
13 getstatic #2 <java/lang/System.out>
16 iload_1
17 invokevirtual #3 <java/io/PrintStream.println>
20 return
dcmpg
、dcmpl
、fcmpg
、fcmpl
、lcmp
fcmpg
和 fcmpl
两个指令,它们的区别在于数字比较时,若遇到NaN值,处理结果不同。指令dcmpl
和 dcmpg
类似。lcmp
针对long型整数,由于long型整数没有NaN值,故无需准备两套指令。举例:
指令 两个指令的不同之处:若遇到NaN值, 类型转换指令说明 宽化类型转换(Widening Numeric Conversions) 转换规则: Java虚拟机直接支持以下数值的 宽化类型转换(widening numeric conversion, 小范围向大范围类型的安全转换)。并不需要强制类型转换,包括: 简化为:int --> long --> float --> double 精度损失问题: 尽管宽化类型转换实际上是可能发生精度丢失的,但是这种转换永远不会导致Java虚拟机抛出运行时异常。 补充说明: 从byte、char、short类型到int类型的宽化类型转换实际上是不存在的。对于byte类型转为int,虚拟机并没有做实质性的转化处理,知识简单地通过操作数栈交换了两个数据。而将byte转为long时,使用的是 Java源代码: JVM字节码: 窄化类型转换(Narrowing Numeric Conversion) 转换规则: Java虚拟机直接支持以下数值的 窄化类型转换: Java 代码: JVM字节码: 精度损失问题: 窄化类型转换可能会导致转换结果具备 不同的正负号、不同的数量级。因此,转换过程很可能会导致数值丢失精度。 尽管数据类型窄化转换可能会发生上限溢出、下限溢出、精度丢失等情况,但是Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常。 补充说明: 当将一个浮点值转换为整数类型T(T限于int或long类型之一)的时候,将遵循以下转换规则: 当将一个 double 类型窄化为 float 类型时,将遵循以下转换规则: 通过向最近数舍入模式舍入一个可以使用float类型标识的数字。最后结果根据下面这3条规则判断: 例1: 例2: Java 是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支持。有一系列指令专门用于对象操作,可进一步细分为创建指令、字段访问指令、数组操作指令、类型检查指令。 虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。 创建类实例的指令: 创建类实例的指令: 创建数组的指令: 上述创建指令可以用于创建对象/数组,由于对象、数组在Java中的广泛应用,这些指令的使用频率也非常高。 类实例举例: Java源代码: JVM字节码: 数组实例举例: Java源代码: JVM字节码: 对象创建后,就可以通过 对象访问指令获取对象实例/数组实例中的字段/数组元素。 举例: 以 对应的字节码指令: 数组操作指令主要有: 说明: 举例: Java源代码: JVM字节码: 检查 类实例/数组实例 类型的指令: 举例: Java源代码: JVM字节码: 方法调用指令: 这5条 指令用于方法调用: 部分举例: 实现类 ==> 接口 强制类型转换: 接口的特殊方法: 方法调用结束前,需要进行返回。方法返回指令是 根据返回值的类型区分 的。 举例: 通过 如果当前返回的是 synchronized 方法,那么还会执行一个隐含的 最后,会丢弃当前这个方法的整个栈帧,恢复调用者的栈帧,并将控制权转交给调用者。 如同操作一个普通数据结构中的栈那样,JVM提供的操作数栈管理指令,可以用于直接操作操作数栈的指令。 这类指令包括如下内容: 这些指令属于通用型,对栈的 压入/弹出 无需指明数据类型。 说明: 代码举例: 例1: 例2: 程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为:比较指令(也归入算数指令)、条件跳转指令、比较条件跳转指令、多条件分支跳转指令、无条件跳转指令。 举例: 指令 两个指令的不同之处:若遇到NaN值, 数值类型的数据,才可以比较大小(byte\short\char\int : long\float\double) boolean、引用数据类型不能比较大小。 条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后进行条件跳转。 条件跳转指令: 它们的统一含义为:弹出栈顶元素,测试它是否满足某一条件,如果满足条件,则跳转到给定位置。 具体说明: 注意: 比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较、跳转两个步骤合二为一。 这类指令有: 其中指令助记符加上 具体说明: 这些指令都接收两个字节的操作数作为参考,用于计算跳转的位置 。同时,在执行指令时,栈顶需要准备两个元素进行比较。指令执行完成后,栈顶的两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下一条语句。 多条件分支跳转指令是专为switch-case语句设计的,主要有: 从助记符上看,两者都是switch语句的实现,它们的区别: 指令 指令 举例: 例1 - tableswitch: Java源代码: JVM字节码: 例2 - lookupswitch: Java源代码: JVM字节码: 例3 - 字符串: Java源代码: JVM字节码 (先比较hashCode后equals比较值): 目前主要的无条件跳转指令为 如果指令偏移量太大,超过双字节的带符号整数的范围,则可以使用指令 指令 异常及异常处理: 在Java程序中显示抛出异常的操作(throw语句)都是由 除了使用throw语句显示抛出异常情况之外,JVM规范还规定了许多运行时异常在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在 注意: 正常情况下,操作数栈的压入/弹出操作都是一条条指令完成的。唯一的例外情况是 在抛异常时,Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。 在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(早期使用 如果一个方法定义了一个 try-catch 或者 try-finally 的异常处理,就会创建一个异常表。它包含了每个异常处理或finally块的信息。异常表保存了每个异常处理信息。比如: 当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法 (在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。 不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标。 举例: 思考: Java虚拟机支持两种同步结构:方法级同步 和 方法内一般指令序列级同步,这两种同步都是是使用 方法级同步:隐式的,即无需通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的 当调用方法时,调用指令将会检查方法的 举例: JVM字节码: 访问标识: 说明: 这段代码和普通的无同步操作的代码没有什么不同,没有使用 同步一段指令集序列,通常是由Java中的synchronized语句块来表示的。JVM的指令集有 当一个线程进入同步代码块时,它使用 当线程退出同步块时,需要使用 指令 举例: Java 源代码: JVM字节码: 说明: 编译器必须确保无论方法通过何种方式完成,方法调用过的每条 为了保证在方法异常完成时,fcmpg
和 fcmpl
都从栈中弹出两个操作数,并将他们做比较,设栈顶的元素为v2,栈顶顺位第2位的元素为v1。若 v1=v2 则压入0;若 v1>v2 则压入1;若v1fcmpg
会压入1,而 fcmpl
会压入-1。4. 类型转换指令
4.1 宽化类型转换
i2l
、i2f
、i2d
l2f
、l2d
f2d
int i = 123123123; //3组
float f = i; //f : 123123120.0F
long l = 123123123123123123L; //6组
double d = l; //d : 123123123123123120.0
i2l
,可以看到在内部byte等同于int类型,类似的还有short、char类型,这种处理方式有两个特点:
public void upCast(byte b) { // short/char 与 byte 类似
int i = b;
long l = b;
double d = b;
}
0 iload_1 //视为int
1 istore_2
2 iload_1 //视为int
3 i2l //int to long
4 lstore_3
5 iload_1 //视为int
6 i2d //int to double
7 dstore 5
9 return
4.2 窄化类型转换
i2b
、i2s
、i2c
。l2i
。f2i
、f2l
。d2i
、d2l
、d2f
。public void downCast() {
long l = 10L;
int i = (int) l;
byte b1 = (byte) l; //double、float与long类似,需2个字节码进行转换
}
0 ldc2_w #2 <10>
3 lstore_1
4 lload_1
5 l2i //int i = (int) l;
6 istore_3
7 lload_1
8 l2i //byte b1 = (byte) l;
9 i2b //byte b1 = (byte) l;
10 istore 4
12 return
public void downCastLoss() {
int i = 128;
byte b = (byte) i;
System.out.println(b); //b : -128
}
public void downCastSpecial() {
// NaN:非数字
double d1 = Double.NaN;
int i = (int) d1;
System.out.println(i); //i : 0
// POSITIVE_INFINITY:无穷大
double d2 = Double.POSITIVE_INFINITY;
long l = (long)d2; //l : 9223372036854775807
int j = (int)d2; //j : 2147483647
System.out.println(l == Long.MAX_VALUE); //true
System.out.println(j == Integer.MAX_VALUE); //true
}
public void downCastSpecial() {
// NaN:非数字
float f1 = (float) d1;
System.out.println(f1); //NaN
// POSITIVE_INFINITY:无穷大
float f2 = (float) d2;
System.out.println(f2); //Infinity
}
5. 对象的创建与访问指令
5.1 创建指令
new
new
:它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。
newarray
、anewaaray
、multianewarray
。
newarray
:创建基本类型数组anewarray
:创建引用类型数组multianewarray
:创建多维数组
public void newInstance() {
Object obj = new Object();
File file = new File("abc");
}
0 new #2 <java/lang/Object> //堆中开辟空间,创建对象实例
3 dup //在操作数栈复制一份引用,用于下一步调用构造器方法(调用后出操作数栈)
4 invokespecial #1 <java/lang/Object.<init>> //调用构造器方法
7 astore_1 //存储至局部变量表,所有引用出操作数栈
8 new #3 <java/io/File>
11 dup
12 ldc #4 <abc> //加载参数
14 invokespecial #5 <java/io/File.<init>>
17 astore_2
18 return
public void newArray() {
int[] intArray = new int[10]; // newarray 10 (int)
Object[] objArray = new Object[10]; // anewarray #2
0 bipush 10
2 newarray 10 (int)
4 astore_1
5 bipush 10
7 anewarray #2 <java/lang/Object>
10 astore_2
11 bipush 10
13 bipush 10
15 multianewarray #6 <[[I> dim 2
19 astore_3
20 bipush 10
22 anewarray #7 <[Ljava/lang/String;>
25 astore 4
27 return
5.2 字段访问指令
getstatic
、putstatic
getfield
、putfield
getxxxx
用于取值,而putxxxx
用于赋值。
getstatic
指令为例,它含有一个操作数,为指向常量池的Fieldref索引,它的作用就是获取Fieldref指定的对象/值,并将其压入操作数栈。public void sayHello() {
System.out.println("hello");
}
0 getstatic #8 <java/lang/System.out>
3 ldc #9 <hello>
5 invokevirtual #10 <java/io/PrintStream.println>
8 return
getstatic
图示:5.3 数组操作指令
xastore
和 xastore
(这里a代表array)指令。具体为:
baload
、caload
、saload
、iaload
、laload
、faload
、daload
、aaload
bastore
、castore
、sastore
、iastore
、lastore
、fastore
、dastore
、aastored
数组类型
加载指令
存储指令
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
arraylength
xaload
表示将数组的元素压栈,比如 saload
、caload
分别表示压入short 数组 和 char 数组。指令 xaload
在执行时,要求操作数栈中栈顶元素为 数组索引i,栈顶顺位第2个元素为 数组引用a,该指令会弹出栈顶这两个元素,并将a[i]压入操作数栈。xastore
则专门针对数组操作,以iastore
为例,它用于给一个int数组的给定索引赋值。在iastore
执行前,操作数栈顶需要以此准备3个元素:值、索引、数组引用,iastore
会弹出这3个值,并将值赋给数组中指定索引的位置。
public void setArray() {
int[] intArray = new int[10];
intArray[3] = 20; //数组赋值
System.out.println(intArray[1]);
}
0 bipush 10
2 newarray 10 (int)
4 astore_1
5 aload_1 //数组引用
6 iconst_3 //索引
7 bipush 20 //值
9 iastore //==>iastore
10 getstatic #8 <java/lang/System.out>
13 aload_1 //数组引用
14 iconst_1 //索引
15 iaload //==>iaload,值:压入操作数栈
16 invokevirtual #11 <java/io/PrintStream.println>
19 return
5.4 类型检查指令
instanceof
、checkcast
。
checkcast
用于检查 类型强制转换 是否可以进行。如果可以进行,那么 checkcast
指令不会改变操作数栈,否则它会抛出 ClassCastException
异常。instantceof
用于 判断 给定对象是否为某一个 类的实例,它会将判断结果压入操作数栈。
public String checkCast(Object obj) {
if (obj instanceof String) {
return (String) obj;
}
return null;
}
0 aload_1
1 instanceof #12 <java/lang/String> //instanceof:判断目标类实例
4 ifeq 12 (+8) //若if条件不满足,跳转至12
7 aload_1
8 checkcast #12 <java/lang/String> //checkcast:类型强制转换
11 areturn
12 aconst_null
13 areturn
6. 方法调用与返回指令
6.1 方法调用指令
invokevirtual
、invokeinterface
、invokespecial
、invokestatic
、invokedynamic
invokevirtual
指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派 / 动态分派),支持多态 (支持override)。这也是Java语言中 最常见的方法分派方式。invokeinterface
指令:用于 调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。invokespecial
指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法、父类方法(super)。这些方法都是 静态类型绑定 的 (无法override),不会在调用时进行动态派发 (静态分派)。invokestatic
指令:用于调用命名 类中的类方法(所有static方法, 与权限访问修饰符无关)。它们是 静态绑定 的 (无法override)。invokedynamic
指令:调用动态绑定的方法(JDK 1.7+ 引入该指令)。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。
invokedynamic
指令的分派逻辑是由用户所设定的引导方法决定的。
Thread t1 = new Thread;
((Runnable)t1).run(); //invokeinterface
public class InterfaceMethodTest {
public static void main(String[] args) {
AA aa = new BB();
aa.methodDefault(); //invokeinterface
AA.methodStatic(); //invokestatic
}
}
interface AA {
static void methodStatic() {}
default void methodDefault() {}
}
class BB implements AA {
}
6.2 方法返回指令
ireturn
(当返回值是 boolean、byte、char、short、int 类型时使用)、lreturn
、freturn
、dreturn
、areturn
return
指令供 声明为void的方法、实例初始化方法(
返回类型
返回指令
void
return
int/boolean/byte/char/short
ireturn
long
lreturn
float
freturn
double
dreturn
reference
areturn
ireturn
指令,将当前函数操作数栈的顶层元素弹出,并将这个元素压入调入者函数的操作数栈中(因为调用者非常关心函数的返回值),所有在当前函数操作数栈中的其他元素都会被丢弃。monitorexit
指令,退出临界区。7. 操作数栈管理指令
pop
、pop2
dup
、dup2
、dup1_x1
、dup1_x2
、dup2_x1
、dup2_x2
swap
。Java虚拟机没有提供交换两个64位数据类型(long、double)数值的指令。nop
,一个非常特殊的指令,它的字节码为0x00
。和汇编语言中的nop一样,它表示什么都不做。这条指令一般可用于调试、占位等。
_x
的指令是复制栈顶数据并压入栈顶。包括两个指令:dup
、dup2
。dup
的系数代表要复制的Slot个数。
dup
开头的指令用于复制1个Slot的数据。例如 1个int / 1个reference 类型数据。dup2
开头的指令用于复制2个Slot的数据。例如1个long / 2个int / 1个int + 1个float类型数据。_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数值出栈。例如:2个int类型数值
public void print() {
Object obj = new Object();
String info = obj.toString(); //astore_2
obj.toString(); //pop
}
public void foo() {
bar(); //pop2
}
public long bar() {
return 0;
}
8. 控制转移指令
8.1 比较指令
dcmpg
、dcmpl
、fcmpg
、fcmpl
、lcmp
fcmpg
和 fcmpl
两个指令,它们的区别在于数字比较时,若遇到NaN值,处理结果不同。指令dcmpl
和 dcmpg
类似。lcmp
针对long型整数,由于long型整数没有NaN值,故无需准备两套指令。fcmpg
和 fcmpl
都从栈中弹出两个操作数,并将他们做比较,设栈顶的元素为v2,栈顶顺位第2位的元素为v1。若 v1=v2 则压入0;若 v1>v2 则压入1;若v1fcmpg
会压入1,而 fcmpl
会压入-1。8.2 条件跳转指令
ifeq
、iflt
、ifle
、ifne
、ifgt
、ifge
、ifnull
、ifnonnull
。这些指令都接收两个字节的操作数,用于计算跳转的位置 (16位符号整数作为当前位置的offset)。
指令
说明
ifeq
当栈顶int类型数值等于0时跳转
ifne
当栈顶int类型数值不等于0时跳转
iflt
当栈顶int类型数值小于0时跳转
ifle
当栈顶int类型数值小于等于0时跳转
ifgt
当栈顶int类型数值大于0时跳转
ifge
当栈顶int类型数值大于等于0时跳转
ifnull
为null时跳转
ifnonnull
不为null时跳转
8.3 比较条件跳转指令
if_icmpeq
、if_icmpne
、if_icmplt
、if_icmpgt
、if_icmple
、if_icmpge
、ifacmpne
。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
比较栈顶两引用类型数值,当结果不相等时跳转
8.4 多条件分支跳转指令
tableswitch
和lookupswitch
。
指令
说明
tableswitch
用于switch条件跳转, case值连续
lookupswitch
用于switch条件跳转, case值不连续
tableswitch
要求 多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量的位置,因此效率比较高。lookupswitch
内部 存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低。tableswitch
的 case 值是连续的,因此只需要记录最低值、最高值,以及每一项对应的offset偏移量,根据给定的index值通过简单的计算即可直接定位到offset。lookupswitch
处理离散的case值,但是出于效率考虑,将case-offset对按照case值大小排序,给定index时,需要查找与index相等的case,获得其offset,如果找不到则跳转到default。
public void switch1(int select) {
int num;
switch (select) {
case 1:
num = 10;
break;
case 2:
num = 20;
break;
case 3:
num = 30;
break;
default:
num = 40;
break;
}
}
0 iload_1
1 tableswitch 1 to 3 1: 28 (+27)
2: 34 (+33)
3: 40 (+39)
default: 46 (+45)
28 bipush 10
30 istore_2
31 goto 49 (+18)
34 bipush 20
36 istore_2
37 goto 49 (+12)
40 bipush 30
42 istore_2
43 goto 49 (+6)
46 bipush 40
48 istore_2
49 return
public void switch2(int select) {
int num;
switch (select) {
case 100:
num = 10;
break;
case 200:
num = 20;
break;
case 300:
num = 30;
break;
default:
num = 40;
break;
}
}
0 iload_1
1 lookupswitch 3
100: 36 (+35)
200: 42 (+41)
300: 48 (+47)
default: 54 (+53)
36 bipush 10
38 istore_2
39 goto 57 (+18)
42 bipush 20
44 istore_2
45 goto 57 (+12)
48 bipush 30
50 istore_2
51 goto 57 (+6)
54 bipush 40
56 istore_2
57 return
public void switchStr(String season) {
switch (season) {
case "SPRING":break;
case "SUMMER":break;
case "AUTUMN":break;
case "WINTER":break;
}
}
aload_1
1 astore_2
2 iconst_m1
3 istore_3
4 aload_2
5 invokevirtual #4 <java/lang/String.hashCode>
8 lookupswitch 4
-1842350579: 52 (+44)
-1837878353: 66 (+58)
-1734407483: 94 (+86)
1941980694: 80 (+72)
default: 105 (+97)
52 aload_2
53 ldc #5 <SPRING>
55 invokevirtual #6 <java/lang/String.equals>
58 ifeq 105 (+47)
61 iconst_0
62 istore_3
63 goto 105 (+42)
66 aload_2
67 ldc #7 <SUMMER>
69 invokevirtual #6 <java/lang/String.equals>
72 ifeq 105 (+33)
75 iconst_1
76 istore_3
77 goto 105 (+28)
80 aload_2
81 ldc #8 <AUTUMN>
83 invokevirtual #6 <java/lang/String.equals>
86 ifeq 105 (+19)
89 iconst_2
90 istore_3
91 goto 105 (+14)
94 aload_2
95 ldc #9 <WINTER>
97 invokevirtual #6 <java/lang/String.equals>
100 ifeq 105 (+5)
103 iconst_3
104 istore_3
105 iload_3
106 tableswitch 0 to 3 0: 136 (+30)
1: 139 (+33)
2: 142 (+36)
3: 145 (+39)
default: 145 (+39)
136 goto 145 (+9)
139 goto 145 (+6)
142 goto 145 (+3)
145 return
8.5 无条件跳转指令
goto
。指令 goto
接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。goto_w
,它与goto
有相同作用,但是它接收4个字节的操作数,可以表示更大的地址范围。goto
指令 常配合 比较条件跳转指令 以实现循环结构。jsr
、jsr_w
、ret
虽也是无条件跳转指令,但主要用于 try-finally 语句,且已经被虚拟机逐渐废弃。
指令
说明
goto
无条件跳转
goto_w
无条件跳转 (宽索引)
jsr
跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶
jsr_w
跳转至指定32位offset位置,并将jsr下一条指令地址压入栈顶
ret
返回至由指定的局部变量所给出的指令位置(一般与jsr、jsr_w联合使用)
9. 异常处理指令
athrow
9.1 抛出异常指令
athrow
指令athrow
指令来实现的。idiv
或ldiv
指令中抛出 ArithmeticException
异常。9.2 异常处理与异常表
9.2.1 异常处理
jsr
、jsr_w
指令),而是 采用异常表来完成的。9.2.2 异常表
public void tryCatch() {
try {
FileInputStream fis = new FileInputStream("ABC.txt");
String info = "hello!";
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
public static String func(){
String str = "hello";
try {
return str;
}finally {
str = "ABC";
}
}
10. 同步控制指令
monitor
来支持的。10.1 方法级同步
ACC_SYNCHRONIZED
访问标志得知一个方法是否声明为同步方法。ACC_SYNCHRONIZED
访问标志是否设置。
private int i = 0;
public synchronized void add() {
i++;
}
0 aload_0
1 dup
2 getfield #2 <com/ljw/demo05/SyncTest.i>
5 iconst_1
6 iadd
7 putfield #2 <com/ljw/demo05/SyncTest.i>
10 return
monitorenter
和monitorexit
进行同步区控制。这是因为,对于同步方法而言,当虚拟机通过方法的访问标识符判断是一个同步方法时,会自动在方法调用前进行加锁,当同步方法执行完毕后,不管方法是正常结束还是由异常抛出,均会由虚拟机释放这个锁。因此,对于同步方法而言,monitorenter
和monitorexit
指令是隐式存在的,并未直接出现在字节码中。10.2 方法内指定指令序列级同步
monitorenter
和 monitorexit
两条指令来支持 synchronized关键字的语义。monitorenter
指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块。monitorexit
声明退出。在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态。monitorenter
和monitorexit
在执行时,都需要在操作数栈顶压入对象,之后monitorenter
(堆内实例的对象头锁状态标识: 0→1, 其他线程将无法对其进行操作)和monitorexit
(堆内实例的对象头锁状态标识: 1→0, 其他线程可以对其进行操作)的锁定和释放都是针对这个对象的监视器进行的。
private int i = 0;
private Object obj = new Object();
public void subtract() {
synchronized (obj) {
i--;
}
}
monitorenter
指令都必须执行其对应的monitorexit
指令,而无论这个方法是正常结束还是异常结束。monitorenter
和 monitorexit
指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit
指令。