文章目录
一、概述
Java虚拟机是基于操作数栈而非寄存器的架构,其操作指令由操作码
和操作数(0或多个操作数)
组成。
- 操作码:操作码的长度为一个字节(0~255),因此操作码总数不能超过256条。
- 操作数:一条指令可以有零或者多个操作数,且操作数可以是1个或者多个字节。
关联文章:
- 《JVM(一) — Class 文件结构》
- 《JVM(二) — 字节码指令》
- 《JVM(三) — Java虚拟机运行时内存结构》
- 《JVM(四) — 垃圾回收机制》
- 《JVM(五) — 类加载机制》
- 《JVM(六) — JVM面试问题》
- 《JVM — 字节码文件分析》
二、指令
按照字节码操作用途,大致可以分为如下9类。
- 加载和存储指令
- 运算指令
- 类型转换指令
- 对象创建和访问指令
- 操作数栈管理指令
- 控制转移指令
- 方法调用和返回指令
- 异常处理指令
- 同步指令
具体的详情可以查看文档:《虚拟机规范》
2.1 加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量和操作数栈之间来回传输。
局部变量操作指令:load
& store
load指令: 将局部变量表中指定位置的指定类型变量加载到操作数栈的栈顶。
store指令: 将操作数栈的栈顶相应类型数据保存到局部变量表的指定位置。
进栈操作指令 | 含义 | 出栈操作指令 | 含义 |
---|---|---|---|
iload | 第1个int型变量进栈 | istore | 栈顶int数值存入第1局部变量 |
iload_0 | 第1个int型变量进栈 | istore_0 | 栈顶int数值存入第1局部变量 |
iload_1 | 第2个int型变量进栈 | istore_1 | 栈顶int数值存入第2局部变量 |
iload_2 | 第3个int型变量进栈 | istore_2 | 栈顶int数值存入第3局部变量 |
iload_3 | 第4个int型变量进栈 | istore_3 | 栈顶int数值存入第4局部变量 |
lload_n | 第n个long型变量进栈 | lstore_n | 栈顶long数值存入第n局部变量 |
fload_n | 第n个float型变量进栈 | fstore_n | 栈顶float数值存入第n局部变量 |
dload_n | 第n个double型变量进栈 | dstore_n | 栈顶double数值存入第n局部变量 |
aload_n | 第n个ref(引用)型变量进栈 | astore_n | 栈顶ref数值存入第n局部变量 |
常量操作指令:bipush
、sipush
、ldc
、iconst_<i>
等
操作指令 | 含义 |
---|---|
bipush | byte型常量进栈 |
sipush | short型常量进栈 |
– | – |
aconst_null | null进栈 |
iconst_m1 | int型常量-1进栈 |
iconst_0 | int型常量0进栈 |
iconst_1 | int型常量1进栈 |
iconst_2 | int型常量2进栈 |
iconst_3 | int型常量3进栈 |
iconst_4 | int型常量4进栈 |
iconst_5 | int型常量5进栈 |
– | – |
lconst_0 | long型常量0进栈 |
fconst_0 | float型常量0进栈 |
dconst_0 | double型常量0进栈 |
常量池操作 | 含义 |
---|---|
ldc | int、float或String型常量从常量池推送至栈顶 |
ldc_w | int、float或String型常量从常量池推送至栈顶(宽索引) |
ldc2_w | long或double型常量从常量池推送至栈顶(宽索引) |
2.2 运算指令
运算或算数指令用于对两个操作数栈上的值进行某种特定运算,并将结果重新存入到操作栈顶。算术指令大体分为两种:对整型数据进行运算、对浮点型数据进行运算。
- 加法指令: iadd、ladd、fadd、dadd
- 减法指令: isub、lsub、fsub、dsub
- 乘法指令: imul、lmul、fmul、dmul
- 除法指令: idiv、ldiv、fdiv、ddiv
- 求余指令: irem、lrem、frem、drem
- 取反指令: ineg、lneg、fneg、dneg
- 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
- 按位或指令: ior、lor
- 按位与指令:: iand、land
- 按位异或指令:: ixor、lxor
- 局部变量表自增指令:iin
- 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
2.3 类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换,这些指令操作一般用于实现用户代码中显示类型转换操作。
宽化类型转换: 虚拟机直接支持如下类型的宽化类型转换。
- int 类型转 long、float、double类型。
- long 类型转 float、double类型。
- float 类型转 double类型。
窄化类型转换: 必须显式地调用类型转换指令来完成转换,并且该过程很可能导致精度丢失。
- 转换指令:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f。
2.4 对象创建和访问指令
- 创建类实例的指令: new
- 创建数组的指令:newarray、anewarray、multianewarray
- 访问类字段(static字段)和实例字段(非static字段)的指令:getfield、putfield、getstatic、putstatic
- 将一个数组元素加载到操作数栈的指令:xaload (x可为b,c,s,i,l,f,d,a)
- 将一个操作数栈的值存储到数组元素的指令: xastore (x可为b,c,s,i,l,f,d,a)
- 获取数组长度的指令:arraylength
- 检查类实例类型的指令:instanceof、checkcast
2.5 操作数栈管理指令
直接操作操作数栈的指令:
- 将操作数栈的栈顶一个或两个元素出栈:pop、pop2。
- 赋制栈顶一个或两个数值并将赋值的值重新亚茹栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
- 将栈最顶端的两个数值互换:swap。
如果对dup相关指令由疑问可以直接查看《虚拟机规范》 中的dup指令(如下图所示)。下图 Operand Stack 代表了操作数栈中dup2指令操作前后的变化。
Form1:
如果操作数栈有两个相同类型的数据时,当执行 dup2 指令时,会赋制栈顶两个元素,最终操作数栈的数据变为:…, value2, value1 —> …, value2, value1, value2, value1
Form2:
如果操作数栈栈顶两个数据类型不同时,当执行 dup2 指令时,只会赋制1个栈顶元素,最终操作数栈的数据变为:…, value1 —> …, value1, value1
2.6 控制转移指令
控制指令是指可以让Java虚拟机有条件或无条件地从指定位置指令继续执行。
- 条件分支:ifeq、iflt、ifnull、ifnonnull、if_icmpeq 等
- 复合分支:tableswitch、lookupswitch
- 无条件分支:goto、goto_w、jsr、jsr_w、ret
2.7 方法调用和返回指令
方法调用
方法调用 | 作用 | 解释 |
---|---|---|
invokevirtual | 调用实例方法 | 虚方法分派 |
invokestatic | 调用类方法 | static方法 |
invokeinterface | 调用接口方法 | 运行时搜索合适方法调用 |
invokespecial | 调用特殊实例方法 | 包括实例初始化方法、父类方法 |
invokedynamic | 由用户引导方法决定 | 运行时动态解析出调用点限定符所引用方法 |
方法返回
方法返回 | 含义 |
---|---|
return | 当前方法返回void |
ireturn | 当前方法返回int |
lreturn | 当前方法返回long |
freturn | 当前方法返回float |
dreturn | 当前方法返回double |
areturn | 当前方法返回ref |
2.8 异常处理指令
Java 程序中显式抛出异常的操作都是由 athrow
指令来实现。异常的处理(catch)不是由字节码指令来实现,而是采用异常表来完成。
2.9 同步指令
方法级的同步和方法内部分代码的同步,都是使用管程(Monitor)来实现的。
同步一段指令集序列,在Java语言中使用 synchronized 语句块来实现,在Java虚拟机层面通过指令集中的 monitorenter
和 monitorexit
两条指令来完成 synchronized 的功能。为了保证monitorenter和monitorexit指令一定能成对的调用(不管方法正常结束还是异常结束),编译器会自动生成一个异常处理器,该异常处理器的主要目的是用于执行monitorexit 指令。