【JVM】字节码指令简介(一)

338 阅读7分钟

什么是字节码指令集

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode) 以随其后的零至多个代表此操作所需参数的操作数(operand) 所构成。虚拟机中许多指并不包含操作数,只有一个操作码。

Java虚拟器解释器工作方式类似下面的这个伪代码

do {
    自动计算PC寄存器以及从PC寄存器的位置取出操作码;
    if(需要操作数) 取出操作数;
    执行操作码;
}while(处理下一次循环)

并不是所有操作码都需要操作数,Java虚拟机是否需要操作数以及操作数的数量取决于操作码

Java虚拟机规范限制了操作码的长度为一个字节,所以指令集的最大长度是由限制的,最大不能超过256个。

字节码指令集中的数据类型

在Java虚拟机的指令集中,大多数的指令都包含了其操作的数据类型信息。他们都是通过指令前缀来区分,其中i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。当然也有一些指令的助记符没有明确用字母指明数据类型,例如dup,它代表的是复制操作数栈栈顶的元素,并且把复制出来的元素压入操作数栈的栈顶。

由于指令集数量的限制,并不是所有类都有对应数据类型的指令,大部分指令都没有支持byte、char、short,甚至没有任何指令支持boolean。

字节码指令详解

字节码指令的分类

字节码指令可以分为

  • 加载和存储指令
  • 运算指令
  • 类型转换指令
  • 对象创建与操作指令
  • 操作数栈管理指令
  • 控制转移指令
  • 方法调用和返回指令
  • 抛出异常指令
  • 同步控制指令

加载和存储指令

加载和存储指令主要用于将数据加载、存储到栈帧中的本地变量表和操作数栈,或者将数据在两者之间来回传递,具体详见下表

指令作用
iload, iload , lload, lload, fload, fload , dload, dload, aload, aload_数值从本地变量表加载到操作数栈
istore, istore , lstore, lstore, fstore, fstore , dstore, dstor, astore, astore_数值从操作数栈存储到本地变量表
bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_ml, iconst_, lconst_, fconst_, dconst_将数值加载到操作数栈
baload, caload, saload, iaload, laload, faload, daload, aaload将数组引用装载到操作数栈
bastore, castore, sastore, iastore, lastore, fastore, dastore, aastore将数组引用存到本地变量表
pop, pop2, dup, dup2, dup_xl, dup2_xl, dup_x2, dup2_x2, swap管理操作数栈中的数据

xload,xstore,xaload,xastore助记符配合前文的字节码指令数据类型理解起来比较简单,这里就不再过多赘述。

数值加载到操作数栈指令的区别

这里我们详细阐述iconst_<i>bipushsipushldcldc2_w的区别,虽然都是将数值加载到操作数栈中,但是不同数据类型,甚至是相同数据类型使用的指令都不相同。我们按照类型逐个详细说明。

  • boolean

boolean会被转换为int,占用一个字节,分别用iconst_1和iconst_0表示

image-20230211113210543

  • byte、short、int

byte、short存储时会被转换成int,在不同的区间使用的操作数不同,具体如下图

image-20230211113226698

  • char

char在存储时也会被转换为int,占用一个slot

image-20230211113319011

  • float

float占用一个slot,使用的是ldc指令

image-20230211113339916

  • long,double

由于加载long和double需要占用两个slot,因此要使用ldc2_w指令

image-20230211113354495

slot的理解

  • 在局部变量表和操作数栈最基本的存储单元都是slot(变量槽)

  • slot中可以存放编译期可知的各种基本数据类型(8种),引用类型(reference), returnAddress类型的变量

  • 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型)

    • long和double占用空间64位,因此要占用两个slot
    • byte 、short 、char在存储前被转换为int,boolean也被转换为int,0表示false ,1表示true

本地变量表中占用slot示意图如下,由于long和double需要占用两个slot,因此后面变量的索引需+2,操作数栈中占用slot与本地变量表中相似,因此就不单独画图展示了

image-20230211115259524

我们拿int举例,代码演示如下

public void loadI() {
        int a = -1;
        int b = 127;
        int c = 32767;
        int d = Integer.MAX_VALUE;
}

编译后字节码如下,可以看到当int取值不同时,加载int到操作数栈的操作码也不相同

 0 iconst_m1                // m代表是负数,因此iconst_m1代表加载-1到操作数栈
 1 istore_1                 
 2 bipush 127               // 加载127到操作数栈
 4 istore_2
 5 sipush 32767             // 加载32767到操作数栈
 8 istore_3
 9 ldc #5 <2147483647>      // 加载2147483647到操作数栈
11 istore 4
13 return
包装类和基本数据类型的指令的区别

演示代码如下

    public static void main(String[] args) {
        boolean bool = true;
        int i = 1;
        long l = 2L;
        short s = 3;
        byte b = 4;
        char c = 5;
        float f = 6.1f;
        double d = 7.1d;
        // =========包装类=======
        Boolean boolx = true;
        Integer ix = 11;
        Long lx = 12L;
        Short sx = 13;
        Byte  bx = 14;
        Character cx = 15;
        Float fx = 16.1f;
        Double dx = 17.1d;
    }

先来看基本数据类型,虚拟机中实际把boolean,short,byte,char都转成了int进行运算,只有int,long,float,double在虚拟机中的数据类型与Java中的数据类型是一致的,其中在操作float与double时,使用ldc指令将初始值压入操作数栈的时候用的l数据类型,使用store指令存储到本地变量表时才使用对应类型的指令。

再来看包装数据类型,通过字节码可以看出定义一个包装类型,虚拟机会调用对应包装类型的valueOf方法获取包装对象,在本地变量表中存的是包装类型的引用。

 0 iconst_1         // boolean -> i
 1 istore_1 
 2 iconst_1         // int -> i
 3 istore_2
 4 ldc2_w #2 <2>    // long -> l
 7 lstore_3
 8 iconst_3         // short -> i
 9 istore 5
11 iconst_4         // byte -> i
12 istore 6
14 iconst_5         // char -> i
15 istore 7
17 ldc #4 <6.1>     // float -> f
19 fstore 8
21 ldc2_w #5 <7.1>  // double -> d
24 dstore 9
26 iconst_1         // Boolean -> a
27 invokestatic #7 <java/lang/Boolean.valueOf : (Z)Ljava/lang/Boolean;>
30 astore 11
32 bipush 11        // Integer -> a
34 invokestatic #8 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
37 astore 12
39 ldc2_w #9 <12>   // Long -> a
42 invokestatic #11 <java/lang/Long.valueOf : (J)Ljava/lang/Long;>
45 astore 13
47 bipush 13        // Short -> a
49 invokestatic #12 <java/lang/Short.valueOf : (S)Ljava/lang/Short;>
52 astore 14
54 bipush 14        // Byte -> a
56 invokestatic #13 <java/lang/Byte.valueOf : (B)Ljava/lang/Byte;>
59 astore 15
61 bipush 15        // Character -> a
63 invokestatic #14 <java/lang/Character.valueOf : (C)Ljava/lang/Character;>
66 astore 16
68 ldc #15 <16.1>   // Float -> a
70 invokestatic #16 <java/lang/Float.valueOf : (F)Ljava/lang/Float;>
73 astore 17
75 ldc2_w #17 <17.1>// Double -> a
78 invokestatic #19 <java/lang/Double.valueOf : (D)Ljava/lang/Double;>
81 astore 18
83 return

运算指令

运算指令会消耗操作数栈上两个值进行某种特定的运算,并把运算结果压入操作数栈顶。运算指令只提供了Java虚拟机的数据类型,没有直接支持byte、short、char和boolean的算数指令,运算指令通过下面的助记符整体还是比较好理解,需要注意的是证书和浮点数的指令在运算的时候出现溢出和被除零除的表现不相同。

注意:

  • 两个正数相加可能,如果出现数据溢出,结果会是一个负数
  • Java虚拟机在处理整型数据时,如果出书是0,会导致虚拟机抛出ArithmeticException
  • 在处理浮点数时,非精确的计算结果必须舍入为可被表示的最接近的精确值,如果有两个值与该值一样接近,将有限选择最低有效位为0的值,这种舍入方式称为想最接近数舍入模式
  • 在处理整型是,Java虚拟机采用的是想0舍入模式,这种舍入方式会导致舍入结果的数字被截断

运算指令可以参考下表

符号作用
iadd, ladd, fadd, dadd
is, ls, fs, ds
imul, lmul, fmul, dmul
idiv, ldiv, fdiv, ddiv
irem, lrem, frem, drem余数
ineg, lneg, fneg, dneg取负
ishl, lshr, iushr, lshl, lshr, lushr移位
ior, lor按位或
iand, land按位与
ixor, lxor按位异或

相关文章

【JVM】字节码指令简介(一) - 掘金 (juejin.cn)

【JVM】字节码指令简介(二) - 掘金 (juejin.cn)

【JVM】字节码指令简介(三) - 掘金 (juejin.cn)

【JVM】字节码指令简介(四)-invokedynamic详解 - 掘金 (juejin.cn)

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情