[三] java虚拟机 JVM字节码 指令集 bytecode 操作码 指令分类用法 助记符
说明,本文的目的在于从宏观逻辑上介绍清楚绝大多数的字节码指令的含义以及分类
只要认真阅读本文必然能够对字节码指令集有所了解
如果需要了解清楚每一个指令的具体详尽用法,请参阅虚拟机规范
指令简介
计算机指令就是指挥机器工作的指示和命令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是计算机的工作过程。
通常一条指令包括两方面的内容: 操作码和操作数,操作码决定要完成的操作,操作数指参加运算的数据及其所在的单元地址。
虚拟机的字节码指令亦是如此含义
class文件相当于JVM的机器语言
class文件是源代码信息的完整表述
方法内的代码被保存到code属性中,字节码指令序列就是方法的调用过程
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)
以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成
虚拟机中许多指令并不包含操作数.只有一个操作码。
如果忽略异常处理,执行逻辑类似
do{ 自动计算pc寄存器以及从pc寄存器的位置取出操作码; if(存在操作数){ 取出操作数; } 执行操作码所定义的操作; }while(处理下一次循环);
操作数的数量以及长度取决于操作码,如果一个操作数的长度超过了一个字节,那么它将大端排序存储,即高位在前的字节序。
例如,如果要将一个16位长度的无符号整数使用两个无符号字节存储起来(将它们命名为byte]和byte2 )
那这个16位无符号整数的值就是: (bytel<<8) | byte2.
字节码指令流应当都是单字节对齐的,只有,tableswitch和lookupswitch两个指令例外 这俩货是4字节为单位的
限制了操作码长度为一个字节 0~255, 但是也就导致操作码个数不能超过256
放弃编译后代码的操作数对齐 也就省略很多填充和间隔符号
限制长度和放弃对齐也尽可能的让编译后的代码短小精干
但是如果向上面那样如果操作码处理超过一个字节的数据时,就必须在运行时从字节流中重建出具体数据结构,将会有一定程度的性能损失
指令详解
说明:
操作码一个字节长度,也就是8位二进制数字,也就是两位十六进制数字
class文件只会出现数字形式的操作码
但是为了便于人识别,操作码有他对应的助记符形式
接下来所有的指令的说明,都是以助记符形式表达的
但是要明确,实际的执行运行并不存在助记符这些东西,都是根据操作码的值来执行
指令本身就是为了功能逻辑运算
运算自然要处理数据
所以说指令的设计是逻辑功能点与数据类型的结合
接下来先看下有哪些数据类型和逻辑功能点
数据类型
上一篇文章中已经说明JVM支持的数据类型
共有9中基本类型
对于基本类型 指令在设计的时候都用一个字母缩写来指代(boolean除外)
byte | short | int | long | float | double | char | reference | boolean |
b | s | i | l | f | d | c | a | 无 |
逻辑功能
加载存储指令 |
算数指令 |
类型转换指令 |
对象的创建于操作 |
操作数栈管理指令 |
控制转移指令 |
方法调用和返回指令 |
抛出异常 |
同步 |
指令基本上就是围绕着上面的逻辑功能以及数据类型进行设计的
当然
也有一些并没有明确用字母指代数据类型,比如arraylength 指令,并没有代表数据类型的特殊字符,操作数只能是一个数组类型的对象
另外还有一些,比如无条件跳转指令goto 则是与数据类型无关的
接下来将会从各个维度对绝大多数指令进行介绍
注意: 在不同的分类中,有些指令是重复的,因为有很多操作是需要处理数据的
也就是说数据类型相关的指令里面可能跟很多逻辑功能点相关联,比如 加载存储指令,可以加载int 可以加载long等
他在我接下来的说明中,可能不仅仅会出现在数据类型相关的指令中
也会出现在加载存储指令的介绍中,请不要疑惑
就是要从多维度介绍这些指令,才能更好地理解他们
指令-相关计算机英语词汇含义
push | push | 按 推动 压入 |
load | load | 加载 装载 |
const | const | 常数,不变的 |
store | store | 存储 保存到 |
add | add | 加法 |
sub | subduction | 减法 |
mul | multiplication | 乘法 |
div | division | 除法 |
inc | increase | 增加 |
rem | remainder | 取余 剩下的留下的 |
neg | negate | 取反 否定 |
sh | shift | 移位 移动变换 |
and | and | 与 |
or | or | 或 |
xor | exclusive OR | 异或 |
2 | to | 转换 转变 变成 |
cmp | compare | 比较 |
return | return | 返回 |
eq | equal | 相等 |
ne | not equal | 不相等 |
lt | less than | 小于 |
le | less than or equal | 小于等于 |
gt | greater than | 大于 |
ge | greater than or equal | 大于等于 |
if | if | 条件判断 如果 |
goto | goto | 跳转 |
invoke | invoke | 调用 |
dup | dump | 复制 拷贝 卸下 丢下 |
指令-数据类型相关的指令
java中的操作码长度只有个字节,所以必然,并不会所有的类型都有对应的操作
Java虚拟机指令集对于特定的操作只提供了有限的类型相关指令
有一些单独的指令可以再必要的时候用来将一些不支持的类型转换为可支持的类型
|
下表中最左边一列的T表示模板,只需要用数据类型的缩写,替换掉T 就可以得到对应的具体的指令
如果下表中为空,说明对这种数据类型不支持这种类型的操作
操作码/类型 | byte | short | int | long | float | double | char | reference |
Tipush | bipush | sipush | ||||||
Tconst | iconst | lconst | fconst | dconst | aconst | |||
Tload | iload | lload | fload | dload | aload | |||
Tstore | istore | lstore | fstore | dstore | astore | |||
Tinc | iinc | |||||||
Taload | baload | saload | iaload | laload | faload | daload | caload | aaload |
Tastore | bastore | sastore | iastore | lastore | fastore | dastore | castore | aastore |
Tadd | iadd | ladd | fadd | dadd | ||||
Tsub | isub | lsub | fsub | dsub | ||||
Tmul | imul | lmul | fmul | dmul | ||||
Tdiv | idiv | ldiv | fdiv | ddiv | ||||
Trem | irem | lrem | frem | drem | ||||
Tneg | ineg | lneg | fneg | dneg | ||||
Tshl | ishl | lshl | ||||||
Tshr | ishr | lshr | ||||||
Tushr | iushr | lushr | ||||||
Tand | iand | land | ||||||
Tor | ior | lor | ||||||
Txor | ixor | lxor | ||||||
i2T | i2b | i2s | i2l | i2f | i2d | |||
l2T | l2i | l2f | l2d | |||||
f2T | f2i | f2l | f2d | |||||
d2T | d2i | d2l | d2f | |||||
Tcmp | lcmp | |||||||
Tcmpl | fcmpl | dcmpl | ||||||
Tcmpg | fcmpg | dcmpg | ||||||
if_TcmpOP | if_icmpOP | if_acmpOP | ||||||
Treturn | ireturn | lreturn | freturn | dreturn | areturn |
从上表的空白处可以看得出来
大部分数据类型相关联的指令,都没有支持整数类型 byte char short ,而且没有任何指令支持boolean类型
因为
编译器会在编译期或者运行期 将byte 和short 类型的数据 带符号扩展 为相应的int类型数据
类似的,boolean 和char类型数据零位扩展为相应的int类型数据
在处理boolean byte short char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理
另外需要格外注意的是,上表是为了呈现部分与数据类型相关联的操作码
并不是说所有的操作码都在上表中,仅仅是和数据类型相关联的才出现在了上表中
|
实际类型与运算类型的对应关系如下,分类后面会说到
实际类型 | 运算类型 | 分类 |
boolean | int | 1 |
int | int | 1 |
byte | int | 1 |
short | int | 1 |
int | int | 1 |
float | float | 1 |
reference | reference | 1 |
returnAddress | returnAddress | 1 |
long | long | 2 |
double | double | 2 |
按照逻辑功能进行划分
加载存储指令
算数指令
运算后的结果自动入栈 运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶.
算术指令分为两种:整型运算的指令和浮点型运算的指令.
无论是哪种算术指令,都使用Java虚拟机的数据类型
由于没有直接支持byte、short、char和boolean类型的算术指令,使用操作int类型的指令代替.
|
加法指令: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
局部变量自增指令:iinc
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
|
再次强调 加add 减sub 乘mul 除div 求余rem 取反neg 移位sh l r表示左右 与and 或or 异或xor 自增inc cmp比较 |
加 减 乘 除 求余 取反 支持 <int i long l float f double d> 四种类型 理解点:常用操作支持四种常用类型 byte short char boolean使用int 移位运算与按位与或异或运算 支持< int i long l > 理解点: 移位与位运算支持整型,byte short char boolean使用int 另外还有long 自增支持< int i > |
补充说明: 关于移位运算,
左移只有一种:
规则:丢弃最高位,往左移位,右边空出来的位置补0
右移有两种:
1. 逻辑右移:丢弃最低位,向右移位,左边空出来的位置补0
2. 算术右移:丢弃最低位,向右移位,左边空出来的位置补原来的符号位(即补最高位)
移位运算的u表示的正是逻辑移位d 和f开头 分别代表double 和float的比较 cmpg 与cmpl 的唯一区别在于对NaN的处理,更多详细内容可以查看虚拟机规范的相关指令 lcmp 比较long类型的值 |
类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换。
这些转换操作一般用于实现用户代码中的显式类型转换操作
或者用来解决字节码指令集不完备的问题
因为数据类型相关指令无法与数据类型一一对应的问题,比如byte short char boolean使用int, 所以必须要转换
|
分为宽化 和 窄化 含义如字面含义,存储长度的变宽或者变窄 宽化也就是常说的安全转换,不会因为超过目标类型最大值丢失信息 窄化则意味着很可能会丢失信息 宽化指令和窄化指令的形式为 操作类型 2 (to) 目标类型 比如 i2l int 转换为long |
宽化指令
int类型到long、float或者double类型
long类型到float、double类型
float类型到double类型
i2l、i2f、i2d
l2f 、l2d
f2d
|
窄化指令 int类型到byte short char类型 long类型到int类型 float类型到int或者long类型 从double类型到int long 或者float类型 i2b 、i2s 、i2c l2i f2i 、f2l d2i 、d2l 、d2f |
对象的创建与访问
实例和数组都是对象 但是Java虚拟机对类实例和数组的创建使用了不同的字节码指令 |
涉及到对象的创建与访问的相关操作有: 1.创建实例对象/数组 2.访问实例变量和类变量 3.加载与存储,对于类实例属于引用类型存取使用加载存储指令,所以此处只有数组有相关操作了 4.还有一些附属信息 数组长度以及检查类实例或者数组类型 |
创建类实例 : new 创建数组的指令 : newarray 分配数据成员类型为基本数据类型的新数组 anewarray 分配数据成员类型为引用类型的新数组 multianewarray 分配新的多维数组 |
类变量声明的时候使用static关键字 访问与存储类中的静态字段也是使用static关键字 getstatic 从类中获取静态字段 putstatic 设置类中静态字段的值 普通的成员实例变量使用field指代 getfield 从对象中获取字段值 putfield 设置对象中的字段的值 |
访问与存储之前介绍过 使用的load 和store 数组也是对象 引用使用a来表示 所以对于数组的存取和访问指令 使用 类型+a+load 或者store 的形式 把一个数组元素加载到操作数栈的指令: byte char short int long float double reference 对应的指令分别是 baload caload saload iaload laload faload daload aaload 把一个操作数栈的值存储到数组元素中的指令: byte char short int long float double reference
对应的指令分别是:
bastore castore sastore iastore lastore fastore dastore aastore
|
获取数组长度的指令 arraylength 检查类实例或者数组类型的指令 instanceof checkcast |
操作数栈管理指令
控制转移指令
控制转移指令可以让Java虚拟机有条件或者无条件的从指定的位置指令继续执行程序 而不是当前控制转移指令的下一条 |
控制转移指令包括
条件转移 复合条件转移以及无条件转移
boolean byte short char都是使用int类型的比较指令
long float double 类型的条件分支比较,会先执行相应的比较运算指令,运算指令会返回一个整型数值到操作数栈中
随后在执行int类型的条件分支比较操作来完成整个分支跳转
显然,虚拟机会对int类型的支持最为丰富
所有的int类型的条件分支指令进行的都是有符号的比较
|
long float double 类型的比较指令 lcmp fcmpl fcmpg dcmpl dcmpg 这五个都比较栈顶上面两个 指定类型的元素,然后将结果 [-1 0 1] 压入栈顶 cmpl与cmpg区别在于对NaN的处理,有兴趣的可以查看Java虚拟机规范 |
条件跳转指令
接下来这六个也就是上面说的配合long float 和double类型条件分支的比较
他们会对当前栈顶元素进行操作判断,只有栈顶的一个元素作为操作数
ifeq 当栈顶int类型元素 等于0时 ,跳转
ifne 当栈顶int类型元素 不等于0 时,跳转
iflt 当栈顶int类型元素 小于0 时,跳转
ifle 当栈顶int类型元素 小于等于0 时,跳转
ifgt 当栈顶int类型元素 大于0 时,跳转
ifge 当栈顶int类型元素 大于等于0 时,跳转
|
类似上面的long float double
int类型 和 reference 当然也有对两个操作数的比较指令,而且还一步到位了
if_icmpeq 比较栈顶两个int类型数值的大小 ,当前者 等于 后者时,跳转
if_icmpne 比较栈顶两个int类型数值的大小 ,当前者 不等于 后者时,跳转
if_icmplt 比较栈顶两个int类型数值的大小 ,当前者 小于 后者时,跳转
if_icmple 比较栈顶两个int类型数值的大小 ,当前者 小于等于 后者时,跳转
if_icmpge 比较栈顶两个int类型数值的大小 ,当前者 大于等于 后者时,跳转
if_icmpgt 比较栈顶两个int类型数值的大小 ,当前者 大于 后者时,跳转
if_acmpeq 比较栈顶两个引用类型数值的大小 ,当前者 等于 后者时,跳转
if_acmpne 比较栈顶两个引用类型数值的大小 ,当前者 不等于 后者时,跳转
|
复合条件跳转指令 tableswitch switch 条件跳转 case值连续 lookupswitch switch 条件跳转 case值不连续 |
无条件转移指令 goto 无条件跳转 goto_w 无条件跳转 宽索引 jsr SE6之前 finally字句使用 跳转到指定16位的offset,并将jsr下一条指令地址压入栈顶 jsr_w SE6之前 同上 宽索引 ret SE6之前返回由指定的局部变量所给出的指令地址(一般配合jsr jsr_w使用) w同局部变量的宽索引含义 |
方法调用和方法返回指令
方法调用和方法返回指令 |
方法调用分为
实例方法接口方法 调用父类私有实力初始化等特殊方法,类静态方法等
以下5条指令用于方法调用:
invokevirtual指令用于调用对象的实例方法
invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
invokestatic指令用于调用类方法(static方法)
invokedynamic 调用动态链接方法 比较复杂,稍后有时间会专门讲解
|
方法的调用与数据类型无关 但是方法的返回指令根据返回值类型进行区分 ireturn boolean byte char short int类型使用 lreturn long freturn float dreturn double areturn reference return void方法 实例初始化方法(构造方法) 类和接口的类初始化方法 |
异常指令
异常处理指令 |
Java程序中显式抛出异常的操作 throw语句,都是由athrow 指令来实现的 除了throw语句显式的抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常 会在其他Java虚拟机指令检测到异常情况时,自动抛出 |
同步指令
同步指令 |
同步一段指令集序列通常是由Java语言中的synchronized 语句块来表示的, Java虚拟机的指令集中有monitorenter monitorexit (monitor +enter/exit) |
至此,虚拟机中的指令集的大致基本设计逻辑以及意图已经基本介绍清楚了,如需要更深一步的了解,请查看虚拟机规范