0%

字节码指令

字节码指令

字节码指令由一个操作码(一个字节长度、代表着特殊含义的数字)以及操作码之后有零个至多个代表此操作所需参数构成

分为九类指令

  • 加载与存储指令
  • 算术指令
  • 类型转换指令
  • 对象的创建与访问指令
  • 方法调用与返回指令
  • 操作数栈管理指令
  • 比较控制指令
  • 异常处理指令
  • 同步控制指令

加载与存储指令

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递

加载是指从局部变量表或常量池中加载到操作数栈,入栈

存储是指将数据从操作数栈存储到局部变量表

局部变量压栈指令

  • xload_<n> x为变量类型i/f/l/d/a,n为0到3,表示将第n个局部变量压入操作数栈
  • xload x为变量类型i/f/l/d/a xload通过指定参数的形式,将局部变量压入操作数栈,使用该命令时,表示局部变量的数量可能超过4个
1
2
3
4
5
6
7
8
public void loadN(int i,long l,APP app){
// iload_1 从LocalVariableTable可以看到,0位是this
System.out.println(i);
// lload_2
System.out.println(l);
// aload 4
System.out.println(app);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void loadN(int, long, com.zhanghe.study.jvm.TestLoad$APP);
descriptor: (IJLcom/zhanghe/study/jvm/TestLoad$APP;)V
flags: ACC_PUBLIC
Code:
stack=3, locals=5, args_size=4
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iload_1
4: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
10: lload_2
11: invokevirtual #4 // Method java/io/PrintStream.println:(J)V
14: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
17: aload 4
19: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
22: return

LocalVariableTable:
Start Length Slot Name Signature
0 23 0 this Lcom/zhanghe/study/jvm/TestLoad;
0 23 1 i I
0 23 2 l J
0 23 4 app Lcom/zhanghe/study/jvm/TestLoad$APP;

常量入栈指令

常量入栈指令的功能是将常量压入操作数栈

  • const指令 对特定的常量入栈,

    iconst_<i>(i从-1到5)、lconst_<l>(l从0到1)、fconst_<f>(f从0到2)、dconst_<d>(d从0到1)、aconst_null

  • push指令 主要有bipush和sipush,bipush接收8位整数(byte类型),sipush接收16位整数(short类型)

  • ldc指令 可以接收8位参数,该参数指向常量池中的int、float、或String的索引,将指定的内容压入栈;如果超过8位的话,还有ldc_w,表示接收两个8位参数;如果接收的是long或double类型的,则使用ldc2_w

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void pushConstLdc(){
// iconst_5
int a = 5;
// bipush 127
int b = 127;
// sipush 128
int c = 128;

// ldc #6 // int 32768
int f = 32768;
// ldc #7 // String 123
String s = "123";

// ldc2_w #8 // long 20l
long l = 20;
// ldc #10 // float 20.2f
float f1 = 20.2f;
// ldc2_w #11 // double 20.12d
double d = 20.12;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public void pushConstLdc();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=11, args_size=1
0: iconst_5
1: istore_1
2: bipush 127
4: istore_2
5: sipush 128
8: istore_3
9: ldc #6 // int 32768
11: istore 4
13: ldc #7 // String 123
15: astore 5
17: ldc2_w #8 // long 20l
20: lstore 6
22: ldc #10 // float 20.2f
24: fstore 8
26: ldc2_w #11 // double 20.12d
29: dstore 9
31: return

LocalVariableTable:
Start Length Slot Name Signature
0 32 0 this Lcom/zhanghe/study/jvm/TestLoad;
2 30 1 a I
5 27 2 b I
9 23 3 c I
13 19 4 f I
17 15 5 s Ljava/lang/String;
22 10 6 l J
26 6 8 f1 F
31 1 9 d D

出栈装入局部变量表指令

出栈装入局部变量表指令用于将操作数栈中的栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量表赋值

  • store指令
    • xstore (x为数据类型,i/l/f/d/a)
    • xstore_n (x为数据类型,n为0到3)
1
2
3
4
5
6
7
8
9
10
public void store(int i,double d){
// 0: iload_1 操作数栈加载i变量入参
// 1: iconst_2 操作数栈加载常量2
// 2: iadd 相加操作
// 3: istore 4 将在操作数栈中相加得到的数值放入局部变量表中索引为4的位置(由于d为double类型,占用了两个位置)
int m = i+2;
// 5: ldc2_w #13 // double 100.0d 操作数栈加载常量100
// 8: dstore_2 // 将double类型放入局部变量表第二个索引位置
d = 100;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void store(int, double);
descriptor: (ID)V
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=3
0: iload_1
1: iconst_2
2: iadd
3: istore 4
5: ldc2_w #13 // double 100.0d
8: dstore_2
9: return
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/zhanghe/study/jvm/TestLoad;
0 10 1 i I
0 10 2 d D
5 5 4 m I

算术指令

  • 加法指令 iadd、ladd、fadd、dadd

  • 减法指令 isub、lsub、fsub、dsub

  • 乘法指令 imul、lmul、fmul、dmul

  • 除法指令 idiv、ldiv、fdiv、ddiv

  • 求余指令 irem、lrem、frem、drem

  • 取反指令 ineg、lneg、fneg、dneg

  • 自增指令 iinc

  • 位运算指令

    • 位移指令 ishl、ishr、iushr、lshl、lshr、lushr
    • 按位或指令 ior、lor
    • 按位与指令 iand、land
    • 按位异或指令 ixor、lxor
  • 比较指令 dcmpg、dcmpl、fcmpg、fcmpl、lcmp 从栈顶取两个数来进行比较

    指令fcmpg和fcmpl的区别在于如果遇到NaN的值,fcmpg会压入1,而fcmpl会压入-1,而long类型没有NaN值,所以只有lcmp

i++和++i的区别

使用字节码来看一下i++和++i的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
// i++和++i的区别
public void add(int i){

// 0: iload_1 操作数栈加载i变量
// 1: iinc 1, 1 执行自增运算
// 4: istore_2 将操作数栈中的栈顶存储到局部变量表中
int a = i++;
System.out.println();
// 11: iinc 1, 1 执行自增运算
// 14: iload_1 操作数栈加载i变量
// 15: istore_3 将操作数栈中的栈顶存储到局部变量表中
int b = ++i;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void add(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=1, locals=4, args_size=2
0: iload_1
1: iinc 1, 1
4: istore_2
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: invokevirtual #15 // Method java/io/PrintStream.println:()V
11: iinc 1, 1
14: iload_1
15: istore_3
16: return
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 this Lcom/zhanghe/study/jvm/TestLoad;
0 17 1 i I
5 12 2 a I
16 1 3 b I

类型转换指令

i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f

对象的创建与访问指令

对象操作中分为创建指令、字段访问指令、数组操作指令、类型检查指令

创建指令

  • 创建类实例的指令 new
  • 创建数组的指令 newarray(基本数据类型数组)、anewarray(引用类型数组)、multianewarray(多维数组)

字段访问指令

通过字段访问指令来获取对象实例或数组实例中的字段或者数组元素

  • 访问类字段static的指令 getstatic、putstatic
  • 访问实例字段 getfield、putfield

数组操作指令

  • xastore (x代表的是数据类型)把操作数栈的值存贷数组元素中
  • xaload (x代表的是数据类型)把数组元素加载到操作数栈
  • arraylength 数组长度指令

类型检查指令

类型检查指令有instanceof和checkcast

  • checkcast用于检查强制类型转换是否可以进行,如果可以进行,checkcast指令不会改变操作数栈,否则会抛出ClassCastException异常
  • 指令instanceof用来判断给定对象是否是某一个类的实例,会将判断结果压入操作数栈

方法调用与返回指令

方法调用指令

  • invokevirtual指令 用于调用对象的实例方法,根据对象的实际类型进行分派,支持多态
  • invokeinterface指令 用于调用接口方法,会在运行时搜索由特定对象所实现的这个接口方法,并找到适合的方法进行调用
  • invokespecial指令 用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法,这些方法都是静态类型绑定的(不存在方法的重写),不会在调用时进行动态派发
  • invokestatic指令 用于调用命名类中的类方法(static方法),静态绑定的
  • invokedynamic指令 用于调用动态绑定的方法,在运行时动态解析出调用点限定符所引用的方法,并执行该方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class TestInvokeInterface {
public static void main(String[] args) {
// invokespecial
InvokeDao invokeDao = new InvokeDaoImpl();
// invokeinterface
invokeDao.test();
// 接口的默认方法 invokeinterface
invokeDao.testDefault();
// 接口的静态方法 invokestatic
InvokeDao.testStatic();
}
}

interface InvokeDao{

String test();

public static void testStatic(){

}

public default void testDefault(){

}

}

class InvokeDaoImpl implements InvokeDao{

@Override
public String test() {
return null;
}
}

方法返回指令

方法返回指令根据返回值类型进行区分

  • ireturn指令(返回值是boolean、byte、char、short、int)、lreturn指令、freturn指令、dreturn指令和areturn指令
  • return指令声明为void的方法、实例初始化方法以及类和接口的类初始化方法

操作数栈管理指令

用于直接操作操作数栈的指令

  • 将一个或两个元素从栈顶弹出,并且直接废弃:pop、pop2
  • 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2(不带x的指令表示复制栈顶数据并压入栈顶,dup的系数表示要复制的slot个数;带x的指令是复制栈顶数据并插入栈顶以下的某个位置,带x指令的复制插入指令,将指令的dup系数和x系数相加,结果就是需要插入的位置)
  • 将栈最顶端的两个slot数值位置交换:swap,没有提供交换两个64位数据类型数值(long、double)的指令
  • 指令nop,字节码为0x00,什么都不做,只是用于调试和占位

控制转移指令

条件跳转指令

条件跳转指令通常和比较指令结合使用,先进行比较指令,在进行条件跳转

条件跳转指令接收两个字节的操作数,用于计算跳转位置

  • ifeq指令 当栈顶int类型的数值等于0时跳转
  • iflt指令 当栈顶int类型的数值小于0时跳转
  • ifle指令 当栈顶int类型的数值小于等于0时跳转
  • ifne指令 当栈顶int类型的数值不等于0时跳转
  • ifgt指令 当栈顶int类型的数值大于0时跳转
  • ifge指令 当栈顶int类型的数值大于等于0时跳转
  • ifnull指令 为null时跳转
  • ifnonnull指令 不为null时跳转

比较条件跳转指令

比较跳转指令类似于比较指令和条件跳转指令的结合

  • if_icmpeq指令 比较栈顶两int类型数值大小,前者等于后者时跳转
  • if_icmpne指令 比较栈顶两int类型数值大小,前者不等于后者时跳转
  • if_icmplt指令 比较栈顶两int类型数值大小,前者小于后者时跳转
  • if_icmpgt指令 比较栈顶两int类型数值大小,前者大于后者时跳转
  • if_icmple指令 比较栈顶两int类型数值大小,前者小于等于后者时跳转
  • if_icmpge指令 比较栈顶两int类型数值大小,前者大于等于后者时跳转
  • if_acmpeq指令 比较栈顶两引用类型数值,结果相等时跳转
  • if_acmpne指令 比较栈顶两引用类型数值,结果不相等时跳转
1
2
3
4
5
6
7
8
9
10
public void test(){
int a = 10;
int b = 20;
// if_icmpne
if(a == b){
System.out.println("相等");
} else {
System.out.println("不相等");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 0 bipush 10
2 istore_1
3 bipush 20
5 istore_2
6 iload_1
7 iload_2
8 if_icmpne 22 (+14)
11 getstatic #2 <java/lang/System.out>
14 ldc #3 <相等>
16 invokevirtual #4 <java/io/PrintStream.println>
19 goto 30 (+11)
22 getstatic #2 <java/lang/System.out>
25 ldc #5 <不相等>
27 invokevirtual #4 <java/io/PrintStream.println>
30 return

多条件分支跳转指令

多条件分支跳转指令是为switch-case设计的

  • tableswitch指令 用于switch条件跳转,case值连续,内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,效率比较高
  • lookupswitch指令 用于switch条件跳转,case值不连续,内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,效率较低

无条件跳转指令

无条件跳转指令主要是goto指令,接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处

  • goto指令 无条件跳转,两个字节的操作数
  • goto_w指令 无条件跳转,四个字节的操作数

异常处理指令

抛出异常指令

athrow指令就是为throw语句来设计的,除了显示的抛出异常外,还要许多的运行时异常会在JVM检测到异常状况时自动抛出

正常情况下,操作数栈的压入弹出都是一条一条指令完成的,唯有在抛出异常时,JVM会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上

try…catch是使用异常表来实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
// hello
System.out.println(test());
// hi
System.out.println(test1());
}

public static String test(){
String str = "hello";
try{
return str;
} finally {
str = "hi";
}
}

public static String test1(){
try{
return "hello";
} finally {
return "hi";
}
}

test方法的字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 0 ldc #6 <hello>
2 astore_0
3 aload_0
4 astore_1
5 ldc #7 <hi>
7 astore_0
8 aload_1
9 areturn
10 astore_2
11 ldc #7 <hi>
13 astore_0
14 aload_2
15 athrow

test1方法的字节码

1
2
3
4
5
6
7
8
0 ldc #6 <hello>
2 astore_0
3 ldc #7 <hi>
5 areturn
6 astore_1
7 ldc #7 <hi>
9 areturn

同步控制指令

方法级的同步是隐式的,无须通过字节码指令来控制,实现在方法调用和返回操作中,虚拟机从方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法

只有在方法内指定同步块,才会有同步控制指令,monitorenter和monitorexit指令来支持synchronized关键字

  • monitorenter指令 当一个线程进入同步代码块时,使用该指令请求进入,如果当前对象的监视器计数器为0,则允许进入;若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则等待,知道对象的监视器计数器为0
  • monitorexit指令 当线程退出同步块时,使用monitorexit声明退出

欢迎关注我的其它发布渠道