JVM指令学习

预备知识:
  1. class文件结构(todo)
  2. 操作数栈(todo)
  3. 局部变量表(todo)

本文粗略给JVM指令进行分类

关于指令集的官方文档(JAVA8)

关于指令集的详细使用方式请参阅:

oracle官方指令集详细文档(JAVA8)

  • 特别注意: 局部变表可以看做是一个数组, 每一个元素是 32bit
  • JAVA虚拟机的指令由1个字节长度的、代表着某种特定含义的数字(操作码)以及跟随其后的零至多个代表此操作所需的参数(操作数)构成!!!~!!
  • 操作数有两种:
    1. 隐式的操作数,在操作数栈中的,一般由load指令加载到操作数栈,操作数的大小根据具体指令来决定
    1. 显式的操作数,直接跟在操作码后面的,下文中方括号里的是显示的操作数,注意显式的操作数每一个都是8bit(1个字节)
  • 下文用这个格式来表示指令操作码 : 助记符 [操作数]

1. 加载和存储指令,数值操作

  • 加载和存储指令用于将数据在栈帧中的局部变量表(todo)和操作数栈(todo)之间来回传输

1.1 加载一个局部变量到操作数栈指令

  • 0x15 : iload [index] : index是一个字节(8位),表示了局部变量表的索引,将指定的int型本地变量推送至栈顶
  • 0x16 : lload [index] : index是一个字节的操作数(8位),表示了局部变量表的索引,将指定的long型本地变量推送至栈顶, 局部变量表中index和index+1的两个32位数构成了一个long型的数
  • 0x17 : fload [index] : index是一个字节的操作数(8位),表示了局部变量表的索引,将指定的float型本地变量推送至栈顶,
  • 0x18 : dload [index] : index是一个index是一个字节的操作数(8位),表示了局部变量表的索引,将指定的double型本地变量推送至栈顶, 局部变量表中index和index+1的两个32位数构成了一个long型的数
  • 0x19 : aload [index] : index是一个字节的操作数(8位),表示了局部变量表的索引,将指定的引用类型本地变量推送至栈顶
  • 0x1a : iload_0 不需要操作数,将局部变量表索引值为0的int变量(32位)推送至栈顶
  • 0x1b : iload_1 : 不需要显示的操作数,将局部变量表索引值为1的int变量(32位)推送至栈顶
  • 0x1c : iload_2 : 不需要操作数,将局部变量表索引值为2的int变量(32位)推送至栈顶
  • 0x1d : iload_3 : 不需要操作数,将局部变量表索引值为3的int变量(32位)推送至栈顶
  • 0x1e : lload_0 : 不需要操作数,将局部变量表索引值为0的32位和索引值为1的32位,组成一个64位的long值,推送至栈顶
  • 0x1f : lload_1 : 不需要操作数,将局部变量表索引值为1的32位和索引值为2的32位,组成一个64位的long值,推送至栈顶
  • 0x20 : lload_2 : 不需要操作数,将局部变量表索引值为2的32位和索引值为3的32位,组成一个64位的long值,推送至栈顶
  • 0x21 : lload_3 : 不需要操作数,将局部变量表索引值为3的32位和索引值为4的32位,组成一个64位的long值,推送至栈顶
  • 0x22 : fload_0 : 与iload_<n>类同
  • 0x23 : fload_1 : 与iload_<n>类同
  • 0x24 : fload_2 : 与iload_<n>类同
  • 0x25 : fload_3 : 与iload_<n>类同
  • 0x26 : dload_0 : 与lload_0类同
  • 0x27 : dload_1 : 与lload_0类同
  • 0x28 : dload_2 : 与lload_0类同
  • 0x29 : dload_3 : 与lload_0类同
  • 0x2a : aload_0 : 与iload_<n>类同
  • 0x2b : aload_1 : 与iload_<n>类同
  • 0x2c : aload_2 : 与iload_<n>类同
  • 0x2d : aload_3 : 与iload_<n>类同

1.2 将一个数值从操作数栈存储到局部变量表指令

  • 0x36 : istore index : index为8位的一个字节,将栈顶int型数值存入局部变量表中索引值为index的32bit位置中
  • 0x37 : lstore [index] : 一个操作数,将栈顶long型数值存入局部变量表索引值为 index和index+1 的两个32bit的位置中
  • 0x38 : fstore [index] : index为8位的一个操作数,将栈顶float型数值存入局部变量表中索引值为index的32bit位置中
  • 0x39 : dstore [index] : 一个操作数,将栈顶double型数值存入局部变量表索引值为 index和index+1 的两个32bit的位置中
  • 0x3a : astore [index] 一个操作数,将栈顶引用型数值存入指定本地变量
  • 0x3b : istore_0 : 不需要操作数,将栈顶int型数值存入第一个本地变量
  • 0x3c : istore_1 : 不需要操作数,将栈顶int型数值存入第二个本地变量
  • 0x3d : istore_2 : 不需要操作数,将栈顶int型数值存入第三个本地变量
  • 0x3e : istore_3 : 不需要操作数,将栈顶int型数值存入第四个本地变量
  • 0x3f : lstore_0 : 与istore_<n>类同
  • 0x40 : lstore_1 : 与istore_<n>类同
  • 0x41 : lstore_2 : 与istore_<n>类同
  • 0x42 : lstore_3 : 与istore_<n>类同
  • 0x43 : fstore_0 : 与istore_<n>类同
  • 0x44 : fstore_1 : 与istore_<n>类同
  • 0x45 : fstore_2 : 与istore_<n>类同
  • 0x46 : fstore_3 : 与istore_<n>类同
  • 0x47 : dstore_0 : 与istore_<n>类同
  • 0x48 : dstore_1 : 与istore_<n>类同
  • 0x49 : dstore_2 : 与istore_<n>类同
  • 0x4a : dstore_3 : 与istore_<n>类同
  • 0x4b : astore_0 : 与istore_<n>类同
  • 0x4c : astore_1 : 与istore_<n>类同
  • 0x4d : astore_2 : 与istore_<n>类同
  • 0x4e : astore_3 : 与istore_<n>类同

1.3 将一个常量加载到操作数栈

  • 0x10 : bipush [byte] : byte为单字节(8位)要操作的常量值,将单字节的常量值(-128~127)推送至栈顶,这个数在栈顶会扩展为32位的一个int数
  • 0x11 : sipush [byte1] [byte2] : 操作数为2个字节(2个byte,一共16位),将两个字节组成一个短整型常量值(-32768~32767)推送至栈顶,这个数在栈顶会扩展为32位的一个int数, value = (byte1<<8)|byte2
  • 0x12 : ldc [index] : index为当前类常量池的索引(预备知识: class的结构), 指向的常量必须是个int或float或指向类/方法类型/方法句柄的符号引用, 将其推送至栈顶
  • 0x13 : ldc_w [indexbyte1] [indexbyte2] : 和ldc差不多,只不过这个索引值是(indexbyte1<<8) | indexbyte2
  • 0x14 : ldc2_w [indexbyte1] [indexbyte2] : 和ldc_w差不多,只不过指向的常量是个long或者double
  • 0x1 : aconst_null : 无操作数将一个null值入栈到操作数栈中
  • 0x2 : iconst_m1 : 将int类型的常量 -1 入栈到操作数栈中
  • iconst_<i> : 将int类型的常量{i | i=0,i=1,i=2,i=3,i=4,i=5}入栈到操作数栈中, 0x3:iconst_0,0x4:iconst_1,0x5:iconst_2,0x6:iconst_3,0x7:iconst_4,0x8:iconst_5
  • 0x11:fconst_0,0x12:fconst_1,0x13:fconst_2 : 将float类型的常量,0.0f,1.0f,2.0f入栈到操作数栈中
  • 0x14:dconst_0,0x15:dconst_1 : 将double类型的常量 0.0d和1.0d入栈到操作数栈中

1.4 wide扩充局部变量表的访问索引的指令:

  • 0xc4 : wide
  • wide指令之后紧跟着被扩展的指令
格式1
  • wide <opcode> [indexbyte1] [indexbyte2]
  • opcode ∈ {iload, fload, aload, lload, dload, istore, fstore, astore, lstore, dstore, ret}

为什么又有一个wide指令?

  1. 在编译class文件的时候,就定义了局部变量表有多大 methods.attributes.LocalVariableTable.local_variable_table_length 这个值是个u2类型=16bit的值,取值范围可以使0~65535,也就是说局部变量表的索引值最大可以是65536
  2. iload指令正常后面跟随一个indexbyte1来表明操作的局部变量表的索引值
  3. 但是indexbyte1是一个无符号的索引值,取值范围是 0~255,
  4. 如果局部变量表很大呢? 索引范围超过了255怎么办??
  5. 这时候wide指令就有用了,他可以帮助iload指令去操作局部变量表中索引值超过255以上的值
格式2
  • wide iinc [indexbyte1] [indexbyte2] [constbyte1] [constbyte2]
  • 对局部变量表索引为:(indexbyte1<<8)|indexbyte2的值做自增操作
  • 自增的量为(constbyte1<<8)|constbyte2

2. 运算指令

Arithmetic Instructions

  • 算术指令用于对两个操作数栈上的值进行某种特定运算, 并把结果重新存入到操作栈顶

  • 大体上运算指令可以分为两种:

    1. 对整型数据进行运算的指令
    1. 对浮点型数据进行运算的指令
  • 整数与浮点数的算术指令在溢出和被零除的时候也有各自不同的行为表现

  • 无论是哪种算术指令, 均是使用Java虚拟机的算术类型来进行计算的, 换句话说不存在直接支持byte、 short、 char和boolean类型的算术指令, 对于上述几种数据的运算, 应使用操作int类型的指令代替

2.1 加法指令

  • 0x60 : iadd 将栈顶两个int型数值相加出栈并将结果压入栈顶
  • 0x61 : ladd 将栈顶两个long型数值相加出栈并将结果压入栈顶
  • 0x62 : fadd 将栈顶两个float型数值相加出栈并将结果压入栈顶
  • 0x63 : dadd 将栈顶两个double型数值相加出栈并将结果压入栈顶

2.2 减法指令

  • 0x64 : isub 将栈顶两个int型数值相减出栈并将结果压入栈顶
  • 0x65 : lsub 将栈顶两个long型数值相减出栈并将结果压入栈顶
  • 0x66 : fsub 将栈顶两个float型数值相减出栈并将结果压入栈顶
  • 0x67 : dsub 将栈顶两个double型数值相减出栈并将结果压入栈顶
  • 栈顶的那个value2是减数,value2下面的value1是被减数

2.3 乘法指令

  • imul、 lmul、 fmul、 dmul

2.4 除法指令

  • idiv、 ldiv、 fdiv、 ddiv
    result = value1 / value2

2.5 求余指令

  • irem、 lrem、 frem、 drem
  • irem结果为: result = value1 - (value1/value2) * value2
  • 其他类同

2.6 取反指令

  • ineg、 lneg、 fneg、 dneg

2.7 位移指令

  • ishl、 ishr、 iushr、 lshl、 lshr、 lushr
  • 哎…规则太多就不一一介绍,还是查文档吧

2.8 按位或指令

  • ior、 lor

2.9 按位与指令

  • iand、 land

2.10 按位异或指令

  • ixor、 lxor

2.11 局部变量自增指令

  • 0x84 : iinc [index] [const] 对局部变量表索引为index(8bit)的值,自增const(8bit)大小
  • 注意这是局部变量自增,所以成员变量自增还是会出现并发问题

2.12 比较指令

3. 类型转换指令

Type Conversion Instructions

类型转换指令可以将两种不同的数值类型相互转换, 这些转换操作一般用于实现用户代码中的显式类型转换操作

或者用来处理本节开篇所提到的字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

Java虚拟机直接支持(即转换时无须显式的转换指令) 以下数值类型的宽化类型转换(Widening Numeric Conversion, 即小范围类型向大范围类型的安全转换) :

  • int类型到long、 float或者double类型
  • long类型到float、 double类型
  • float类型到double类

与之相对的, 处理窄化类型转换(Narrowing Numeric Conversion) 时, 就必须显式地使用转换指令来完成,这些转换指令包括

  • 0x91 : i2b
  • 0x92 : i2c
  • 0x93 : i2s
  • 0x88 : l2i
  • 0x8b : f2i
  • 0x8c : f2l
  • 0x8e : d2i
  • 0x8f : d2l
  • 0x90 : d2f
  • 这些指令都是对当前操作数栈,栈顶的数进行操作
    1. 先对栈顶的数出栈
    1. 将出栈的数值进行转换
    1. 转换后的值入栈
  • 窄化类型转换可能会导致转换结果产生不同的正负号、 不同的数量级的情况, 转换过程很可能会导致数值的精度丢失。

在将int或long类型窄化转换为整数类型T的时候, 转换过程仅仅是简单丢弃除最低位N字节以外的内容, N是类型T的数据类型长度, 这将可能导致转换结果与输入值有不同的正负号。 对于了解计算机数值存储和表示的程序员来说这点很容易理解, 因为原来符号位处于数值的最高位, 高位被丢弃之后, 转换结果的符号就取决于低N字节的首位了。

尽管数据类型窄化转换可能会发生上限溢出、 下限溢出和精度丢失等情况, 但是《Java虚拟机规范》 中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常。

4. 对象创建与访问指令

Object Creation and Manipulation

虽然类实例和数组都是对象, 但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令(在下一章会讲到数组和普通类的类型创建过程是不同的) 。 对象创建后, 就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素, 这些指令包括:

4.1 创建类实例的指令

  • 0xbb : new [indexbyte1] [indexbyte2]
    1. 找到具体的类: 类常量池的索引=(indexbyte1<<8)|indexbyte2 应该是个类或者接口的符号引用
    1. 对其进行初始化操作(这里面的操作可能很复杂)
    1. 再将初始化后的实例的reference类型数据入栈

4.2 创建数组的指令

  • 0xbc : newarray [atype]
  • 0xbd : anewarray [indexbyte1] [indexbyte2]
  • 0xc5 : multianewarray [indexbyte1] [indexbyte2] [dimensions]
  • 具体看文档

4.3 访问类字段指令

  • 0xb2 : getstatic [indexbyte1] [indexbyte2] : 以(indexbyte1<<8)|indexbyte2为索引值 指向常量池中的静态变量的reference 将它的值放入操作数栈中
  • 0xb3 : putstatic [indexbyte1] [indexbyte2] : 和上面的相反,将操作数栈的reference放入静态变量中

4.4 访问实例字段的指令

  • 0xb2 : getfield [indexbyte1] [indexbyte2]
  • 解析:
    1. 当执行getfeild的时候,栈顶一定得是个对象的引用才合法
    1. (indexbyte1<<8)|indexbyte2 是这个对象的类的常量池中的索引,应该指向的是class文件中描述的fields的索引, (就是特么的成员变量)
    1. 将这个实例的成员变量取出,并放入操作站栈顶

  • 0xb5 : putfield [indexbyte1] [indexbyte2]
  • 操作数栈出栈 objectref, value
  • 解析:
    1. (indexbyte1<<8)|indexbyte2计算出objectref的成员变量的索引
    1. value的值赋给objectref的成员变量
    1. objectref, value 出栈

4.5 把一个数组元素加载到操作数栈的指令

4.6 将一个操作数栈的值储存到数组元素中的指令

4.7 取数组长度的指令

4.8 检查实例类型的指令

5. 操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样, Java虚拟机提供了一些用于直接操作操作数栈的指令

5.1 操作数栈出栈指令

  • 0x57 : pop : 单纯的出栈1个元素,32bit
  • 0x58 : pop2 : 单纯的出栈2个元素,64bit

5.2 复制栈顶元素并压栈

  • 0x59 : dup : 复制栈顶数值(数值不能是long或double类型的)并将复制值压入栈顶
  • 0x5c : dup2 : 复制栈顶数值(数值不能是long或double类型的)并将两个复制值压入栈顶
  • 0x5a : dup_x1 : 复制栈顶数值(数值不能是long或double类型的)并将三个(或两个)复制值压入栈顶
  • (0x5d : dup2_x1 : 复制栈顶一个(long或double类型的)或两个(其它)数值并将复制值压入栈顶
  • 0x5b : dup_x2 : 复制栈顶数值(long或double类型的)并将两个复制值压入栈顶
  • 0x5e : dup2_x2 : 复制栈顶数值(long或double类型的)并将三个(或两个)复制值压入栈顶

5.3 将栈顶两个值互换

  • 0x5f : swap : 将栈顶两个值互换

6. 控制转移指令

控制转移指令可以让Java虚拟机有条件或无条件地从指定位置指令(而不是控制转移指令) 的下一条指令继续执行程序,

从概念模型上理解, 可以认为控制指令就是在有条件或无条件地修改PC寄存器的值

6.1 条件分支

6.1.1 整数与零比较的条件分支判断

  • 0x99 : ifeq [branchbyte1] [branchbyte2] : 栈顶元素出栈,并判断当且仅当栈顶的数据=0时,比较结果是否为真,如果为真,跳转偏移量为(branchbyte1<<8)|branchbyte2的指令位置
  • 0x9a : ifne [branchbyte1] [branchbyte2] : 栈顶元素出栈,并判断当且仅当栈顶的数据≠0时,比较结果是否为真,如果为真,跳转偏移量为(branchbyte1<<8)|branchbyte2的指令位置
  • 0x9b : iflt [branchbyte1] [branchbyte2] : 栈顶元素出栈,并判断当且仅当栈顶的数据<0时,比较结果是否为真,如果为真,跳转偏移量为(branchbyte1<<8)|branchbyte2的指令位置
  • 0x9c : ifge [branchbyte1] [branchbyte2] : 栈顶元素出栈,并判断当且仅当栈顶的数据>=0时,比较结果是否为真,如果为真,跳转偏移量为(branchbyte1<<8)|branchbyte2的指令位置
  • 0x9d : ifgt [branchbyte1] [branchbyte2] : 栈顶元素出栈,并判断当且仅当栈顶的数>0时,比较结果是否为真,如果为真,跳转偏移量为(branchbyte1<<8)|branchbyte2的指令位置
  • 0x9e : ifle [branchbyte1] [branchbyte2] : 栈顶元素出栈,并判断当且仅当栈顶的数据<=时,比较结果是否为真,如果为真,跳转偏移量为(branchbyte1<<8)|branchbyte2的指令位置

6.1.2 引用的条件分支判断

  • 0xc6 : ifnull [branchbyte1] [branchbyte2] : 如果为空,跳转(branchbyte1<<8)|branchbyte2指令位置
  • 0xc7 : ifnonnull [branchbyte1] [branchbyte2] : 如果非空,跳转(branchbyte1<<8)|branchbyte2指令位置
  • 0x9f : if_icmpeq : 操作数栈2个32bit的int值比较,且出栈, 如果相等则为真, 则跳转偏移量为(branchbyte1<<8)|branchbyte2的指令位置
  • 0xa0 : if_icmpne : 类同
  • 0xa1 : if_icmplt : 类同
  • 0xa2 : if_icmpge : 类同
  • 0xa3 : if_icmpgt : 类同
  • 0xa4 : if_icmple : 类同
  • 0xa5 : if_acmpeq [branchbyte1] [branchbyte2] : 操作数栈中两个reference类型的比较,如果相等,则为真,其他类同
  • 0xa6 : if_acmpne [branchbyte1] [branchbyte2] : 操作数栈中两个reference类型的比较,如果不相等,则为真,其他类同

6.2 复合条件分支

6.3 无条件分支

  • 0xa7 : goto [branchbyte1] [branchbyte2] : 无条件跳转到(branchbyte1<<8)|branchbyte2指令位置
  • 0xc8 : goto_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4] : 无条件跳转到(branchbyte1<<24)|(branchbyte1<<16)|(branchbyte1<<8)|branchbyte4指令位置, 虽然有4字节, 但是class文件的规范规定了方法的指令长度是个u2数字,也就是说一个方法最多也就 65535 个指令, 这个指令并没什么卵用
  • jsr : 大神说JDK1.4.2开始就不用这个指令了
  • jsr_w : 大神说JDK1.4.2开始就不用这个指令了
  • ret : 大神说JDK1.4.2开始就不用这个指令了
  • 这三个弃用的指令大概意思是,实现try,catch,finally的,jsr把returnAddress入栈之后,跳转finally代码块,finally代码块执行完用ret回到jsr之后的位置继续执行
  • JDK7 以上是把 finally块的指令赋值到原本每个jsr的地方,实现了禁用jsrret的功能,也就是字节码指令冗余了一些

7. 方法调用和返回指令

7.1 方法调用指令

  • 0xb6 : invokevirtual [indexbyte1] [indexbyte2] : 调用实例方法,在class常量池中找方法(indexbyte1<<8)|indexbyte2是索引值,同时操作数栈中的实例reference和参数都出栈传给被调用的方法
  • 0xb9 : invokeinterface [indexbyte1] [indexbyte2] [count] [0] 调用接口方法,同上
  • 备注:
    1. count记录了参数的个数,这个信息也可以从解析出的方法描述符中得到,放在这里是由于历史原因.
    1. 第四个操作数一定是 0 , 是为额外的运算元预留空间,这些运算元用于某些oracle的java虚拟机,用来在运行时用某些专用的伪指令替换invokeinterface指令。因此必须保留以达到后项兼容性。
  • 0xb7 : invokespecial [indexbyte1] [indexbyte2] : 调用实例方法,专门用来调用父类方法/私有方法/和实例初始化方法
  • 0xb8 : invokestatic [indexbyte1] [indexbyte2] : 调用静态方法, 操作数栈中没有实例的reference了
  • 0xba : invokedynamic [indexbyte1] [indexbyte2] [0] [0] : 调用动态方法, (indexbyte1<<8)|indexbyte2指向的是CONSTANT_InvokeDynamic_info,而不是CONSTANT_Methodref_info了 参见invokedynamic指令

7.2 方法返回指令

  • 0xac : ireturn 从操作数栈顶出栈,然后入栈到调用者栈帧的操作数栈中, 同时也将本操作数栈所有值丢弃
  • 0xad : lreturn
  • 0xae : freturn
  • 0xaf : dreturn
  • 0xb0 : areturn
  • 0xb1 : return 本栈帧的操作数栈所有值全部丢弃

8. 异常处理指令

  • 0xbf : athrow 操作数栈栈顶是个异常(Throwable)的reference,把这个异常reference出栈丢给异常处理器(Java里就是catch代码块)处理,如果没有那么该栈帧要出栈了

9. 同步指令

这个指令一般是在使用 synchronized 关键字的时候出现, 需要了解对象头的概念

  • 0xc2 : monitorenter : 尝试获取操作数栈栈顶那个reference指向的对象的monitor(在对象头中), 文档
  • 0xc3 : monitorexit : 退出一个对象的monitor
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值