本文的目的在于从宏观逻辑上介绍清楚绝大多数的字节码指令的含义以及分类
只要认真阅读本文必然能够对字节码指令集有所了解
如果需要了解清楚每一个指令的具体详尽用法,请参阅虚拟机规范
指令简介
计算机指令就是指挥机器工作的指示和命令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是计算机的工作过程。
通常一条指令包括两方面的内容: 操作码和操作数,操作码决定要完成的操作,操作数指参加运算的数据及其所在的单元地址。
虚拟机的字节码指令亦是如此含义
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 |
按照逻辑功能进行划分
加载存储指令
加载存储指令用于局部变量与操作数栈交换数据
以及常量装载到操作数栈
1、将一个局部变量加载到操作栈:
iload、iload<n>、lload、lload<n>、fload、fload<n>、dload、dload<n>、aload、aload_<n>
操作数为局部变量的位置序号 序号从0开始 , 局部变量以slot为单位分配的
将序号为操作数的局部变量slot 的值 加载到操作数栈
指令可以读作:将第(操作数+1)个 X(i l f d a)类型局部变量,推送至栈顶
ps: 操作数+1 是因为序号是从0开始的
2、将一个数值从操作数栈存储到局部变量表:
istore、istore<n>、lstore、lstore<n>、fstore、fstore<n>、dstore、dstore<n>、astore、astore_<n>
操作数为局部变量的位置序号 序号从0开始 , 局部变量以slot为单位分配的
将操作数栈的值保存到序号为操作数的局部变量slot中
指令可以读作:将栈顶 X(i l f d a)类型的数值 保存到 第(操作数+1)个 局部变量中
3、将一个常量加载到操作数栈:
bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconstm1、iconst<i>、lconst<l>、fconst<f>、dconst_<d>
操作数为将要操作的数值 或者常量池行号
指令可以读作:将类型X的值xxx 推送至栈顶 或者是 将 行号为xxx的常量推送至栈顶
4、扩充局部变量表的访问索引的指令:wide
形如 xxx_<n>
以尖括号结尾的代表了一组指令 (例如iload_<n>
代表了iload_0 iload_1 iload_2 iload_3
)
这一组指令都是某个带有一个操作数的通用指令(例如 iload)的特殊形式
对于这些特殊形式来说,他们表面上没有操作数,但是操作数隐含在指令里面了,除此之外,语义与原指令并没有任何的不同
(例如 iload_0
的语义与操作数为0时的iload 语义完全相同)
<>尖括号中的字母表示了指令隐含操作数的数据类型
<n>
表示非负整数 <i>
表示int <l>
表示long <f>
float <d>
double
而byte char short
类型数据经常使用int`来表示
下划线 _ 的后面紧跟着的值就是操作数
需要注意的是 _<n> 的形式不是无限的,对于load 和 store系列指令
对于超过4个,也就是第5个,也就是下标是4 往后
都是直接只用原始形式 iload 4 不再使用_
<n>的形式 所以你不会看到 load_4 load_5....或者store_4 store_5...
对于虚拟机执行方法来说,操作数栈是工作区, 所以数据的流向是对于他 操作数栈 来说的
load就是局部变量数据加载到操作数栈
store就是从操作数栈存储到局部变量表
对于常量只有加载到操作数栈进行使用,没有存储的说法,他也比较特殊
对于上图中的数据交换模型中,操作数栈是可以确定的也是唯一的,栈就在那里,不管你见或不见
对于操作数栈与局部变量交换数据时,需要确定的是 从 哪个局部变量取数据 或者保存到哪个局部变量中
所以load 和 store的操作数都是局部变量的位置
对于操作数栈与常量交换数据,需要确定的是到底加载哪个值到操作数栈或者是从常量池哪行加载
所以加载常量到操作数栈的操作数 是 具体的数值 或者常量池行号
常量加载到操作数栈比较特殊单独说明
他根据<数据类型>以及<数据的取值范围>使用了不同的方式
const指令
该系列命令主要负责把简单的数值类型送到栈顶 。
该系列命令不带参数。只把简单的数值类型送到栈顶时,才使用如下的命令。
比如对应int型该方式只能把-1,0,1,2,3,4,5(分别采用iconst_m1,iconst_0, iconst_1, iconst_2, iconst_3, iconst_4, iconst_5)
送到栈顶。对于int型,其他的数值请使用push系列命令(比如bipush)
指令码 助记符 说明
0x01 aconst_null 将null推送至栈顶
0x02 iconst_m1 将int型(-1)推送至栈顶
0x03 iconst_0 将int型(0)推送至栈顶
0x04 iconst_1 将int型(1)推送至栈顶
0x05 iconst_2 将int型(2)推送至栈顶
0x06 iconst_3 将int型(3)推送至栈顶
0x07 iconst_4 将int型(4)推送至栈顶
0x08 iconst_5 将int型(5)推送至栈顶
0x09 lconst_0 将long型(0)推送至栈顶
0x0a lconst_1 将long型(1)推送至栈顶
0x0b fconst_0 将float型(0)推送至栈顶
0x0c fconst_1 将float型(1)推送至栈顶
0x0d fconst_2 将float型(2)推送至栈顶
0x0e dconst_0 将double型(0)推送至栈顶
0x0f dconst_1 将double型(1)推送至栈顶
简言之 取值 -1~5 时,JVM采用const指令将常量压入栈中
push指令
该系列命令负责把一个整型数字(长度比较小)送到到栈顶。
该系列命令有一个参数,用于指定要送到栈顶的数字。
注意该系列命令只能操作一定范围内的整形数值,超出该范围的使用将使用ldc命令系列。
指令码 助记符 说明
0x10 bipush 将单字节的常量值(-128~127)推送至栈顶
0x11 sipush 将一个短整型常量值(-32768~32767)推送至栈顶
ldc系列
该系列命令负责把数值常量或String常量值从常量池中推送至栈顶。
该命令后面需要给一个表示常量在常量池中位置(编号)的参数 也就是行号,
哪些常量是放在常量池呢?
比如:
final static int id=32768; //32767+1 就不在sipush范围内了
final static float double=8.8
对于const系列命令和push系列命令操作范围之外的数值类型常量,都放在常量池中.
另外,所有不是通过new创建的String都是放在常量池中的
指令码 助记符 说明
0x12 ldc 将int, float或String型常量值从常量池中推送至栈顶
0x13 ldc_w 将int, float或String型常量值从常量池中推送至栈顶(宽索引)
0x14 ldc2_w 将long或double型常量值从常量池中推送至栈顶(宽索引)
ps:
所谓宽索引是指常量池行号 索引的字段长度, ldc 的索引只有8位 ldc_w的索引则有16位
对于宽索引,指令格式为 ldc_w ,indexbyte1,indexbyte2 会计算 (indexbyte1<<8) | indexbyte2 来生成一个指向当前常量池的无符号16位索引
说白了就是寻址长度
简言之就是对于绝大多数的数值,都是存放在常量池中的 将需要使用ldc
对于一小部分可能比较常用的数值,则是可以直接把值当做操作数的 使用const 或者push
wide的含义 宽索引
字节码的指令是单字节的,对于局部变量来说,最多容纳256个局部变量
wide指令就是用于扩展局部变量数的,将8位的索引在扩展8位 也就是16位 最多65536
形式为
wide 要被扩展的操作码比如iload 操作数 (wide iload 257 也就是 wide iload byte1 byte2)
iload操作码是作为wide 操作码的一个操作数来执行的
wide可以修饰 load store ret
如果wide修饰的是iinc 格式有些变化
wide iinc byte1 byte2 constbyte1 constbyte2 本身 iinc为 iinc byte constbyte
扩展后的前两个字节16位为局部变量索引
后两个字节16位计算为 16位带符号的增量
计算的形式依旧是 (constbyte1 << 8) | constbyte2
算数指令
运算后的结果自动入栈
运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶.
算术指令分为两种:整型运算的指令和浮点型运算的指令.
无论是哪种算术指令,都使用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
右移有两种:
- 逻辑右移:丢弃最低位,向右移位,左边空出来的位置补0
- 算术右移:丢弃最低位,向右移位,左边空出来的位置补原来的符号位(即补最高位)
移位运算的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
操作数栈管理指令
操作数栈管理指令,顾名思义就是直接用于管理操作栈的
对于操作数栈的直接操作主要有 出栈/复制栈顶元素 / 以及 交换栈顶元素
出栈, 分为将操作数栈栈顶的几个元素出栈,一个元素或者两个元素
pop表示出栈, 数值代表个数
pop pop2
交换 将栈顶端的两个数值进行交换
swap
dup比较复杂一点
根本含义为复制栈顶的元素然后压入栈
不过涉及到复制几个元素,以及操作数栈的数据类型,所以比较复杂
上面提到过虚拟机处理的数据类型,有分类,分为1 和2两种类型
虚拟机能处理的类型long和double为类型2 其余为类型1 也就是int returnAddress reference等
dup 复制操作数栈栈顶一个元素 并且将这个值压入到栈顶 value必须分类1
形式如下,右侧为栈顶
... , value
... , value , value
dup_x1 复制操作数栈栈顶的一个元素.并插入到栈顶以下 两个值之后
形式如下,右侧为栈顶,value1 插入到了第二个元素value2 下面 value1 和value2 必须分类1
... , value2, value1
... , value1, value2, value1
dup_x2 复制操作数栈栈顶的一个元素. 并插入栈顶以下 2 个 或 3个值之后
形式一 如果 value3, value2, value1 全都是分类1 使用此形式 插入栈顶三个值 以下 也就是value3之下
..., value3, value2, value1 →
..., value1, value3, value2, value1
形式二如果value1 是分类1 value2 是分类2 那么使用此形式 插入栈顶两个值 以下,也就是value2 之下
..., value2, value1 →
..., value1, value2, value1
dup2 复制操作数栈栈顶一个或者两个元素,并且按照原有顺序,入栈到操作数栈
形式一 如果 value2, value1 全都是分类1 使用此形式 复制栈顶两个元素,按照原顺序,插入到栈顶
..., value2, value1 →
..., value2, value1, value2, value1
形式二 如果value 属于分类2 使用此形式 复制栈顶一个元素,插入到栈顶
..., value →
..., value, value
dup2_x1复制操作数栈栈顶一个或者两个元素,并且按照原有顺序 插入栈顶以下 两个或者三个 值 之后
形式一 如果 value3, value2, value1 都是分类1 使用此形式 复制两个元素,插入栈顶下 三个值之后,也就是value3 之后
..., value3, value2, value1 →
..., value2, value1, value3, value2, value1
形式二 如果value1 是分类2 value2 是分类1 使用此形式 复制一个元素,插入到栈顶以下 两个元素之后
..., value2, value1 →
..., value1, value2, value1
dup_x2 复制操作数栈栈顶一个或者两个元素,并且按照原有顺序 插入栈顶以下 两个或者三个 或者四个 值 之后
形式一 全都是分类1 使用此形式 复制两个元素,插入到栈顶 第四个值后面
..., value4, value3, value2, value1 →
..., value2, value1, value4, value3, value2, value1
形式二 如果 value1 是分类2 value2 和 value3 是分类1 中的数据类型 使用此形式 复制一个元素 插入到栈顶 第三个值后面
..., value3, value2, value1 →
..., value1, value3, value2, value1
形式三 如果value 1 value2 是分类1 value3 是分类2 使用此形式 复制两个元素 插入到栈顶 第三个值后面
..., value3, value2, value1 →
..., value2, value1, value3, value2, value1
形式四 当value1 和value2 都是分类2 使用此形式 复制一个元素 插入到栈顶 第二个值后面
..., value2, value1 →
..., value1, value2, value1
上面关于dup的描述摘自 虚拟机规范,很难理解
看起来是非常难以理解的,不妨换一个角度
我们知道局部变量的空间分配分为两种long 和 double 占用2个slot 其他占用一个
操作数栈,每个单位可以表示虚拟机支持的任何的一个数据类型
不过操作数栈其实同局部变量一样,他也是被组织一个数组, 每个元素的数据宽度和局部变量的宽度是一致的
所以对于long 和double占用2个单位长度 对于其他类型占用一个单位长度
虽然外部呈现上任何一个操作数栈可以表示任何一种数据类型,但是内部是有所区分的
如同局部变量表使用两个单位存储时,访问元素使用两个中索引小的那个类似的道理
所以可以把栈理解成线性的数组,
来一个long或者double 就分配两个单位空间作为一个元素
其余类型就分配一个单位空间作为元素
既然栈本身的结构中,线性空间的最小单位的数据宽度同局部变量,
long和double占用两个 也就是下面涉及说到的数据类型的分类1 和 分类2
假设栈的示意结构如下图所示,(只是给出来一种可能每个元素的类型都可能是随机的)
左边表示呈现出来的栈元素 右边是内部的线性形式 我们当做数组好了
对栈元素的处理,显然指的是对于栈元素内部数组的处理
所以自然要分为
到底是直接复制一个单位的数据
还是直接复制两个单位的数据
一次复制占用一个单位空间 的指令 使用dup
一次复制占用两个单位空间 的指令 使用dup2
一次复制占用一个单位空间 时 假设复制的栈顶是array[0]
dup 可以理解为dup_x0
插入到他栈顶的内部线性结构的第(1+0)个元素下面 所以array[0] 对应的必然是一个完整的栈元素 ,必然是分类1 不可能是分类2的一半!
dup_x1
插入到他栈顶的内部线性结构的第(1+1)个元素下面 也就是插到第二个下面 因为array[0] 对应value1为分类1
如果接下来的是分类2的数据,必然接下来的两个单元array[1] 和array[2]是不可分割的,也就是不可能插入到array[1] 后面,所以array[1] 对应value2 也必须是分类1 也就是两个都是分类1
dup_x2
插入到他栈顶的内部线性结构的第(1+2)个元素下面 也就是插到第三个后面,array[0] 对应value1为分类1 为分类1
那么接下来的两个单位array[1] 和array[2],可以是一个分类2 也可以是两个分类1,都是可以的
一次复制占用两个单位的数据类型 时
dup2 可以理解为dup2_x0
插入到他栈顶的内部线性结构的第(2+0)个元素下面
这一次复制的两个单位array[0] 和 array[1], 到 array[1]下面
可能是对应value1 和value2 表示两个分类1 也可能是对应一个value1 表示类型为分类2
dup2_x1 插入到他栈顶的内部线性结构的第(2+1)个元素下面 也就是复制array[0] 和 array[1] 到第三个元素 array[2]的下面
array[0] 和 array[1] 可能分别对应value1 和value2 表示两个分类1 数据 也可能是对应着一个value1表示一个分类2数据
但是array[2] 作为第三个单位,既然能被分割,自然他必须是分类1
所以要么三个都是分类1,要么value1 分类2 value2 分类1
dup2_x2 插入到他栈顶的内部线性结构的第(2+2)个元素下面 也就是复制array[0] 和 array[1] 到第四个内部元素 array[3]的下面
一次复制两个,放到第四个下面
这种情形下的组合就非常多了
全都是分类1的数据
全部都是分类2
array[0] 和 array[1] 对应value1 表示一个分类2数据
array[2] 和 array[3] 对应value2 表示一个分类2数据
array[0] 和 array[1] 对应value1 表示一个分类2数据
array[2] 和 array[3] 对应value2 和 value3表示两个分类1数据
array[0] 和 array[1] 对应value1 和value2 表示两个分类1 数据
array[2] 和 array[3] 对应value3表示一个分类2数据
只需要明确以下几点,就不难理解dup指令
操作数栈指令操作的是栈内部的存储单元,而不是以一个栈元素为单位的
long和double在栈元素内部需要两个存储单元,其余一个存储单元
两个相邻的内部单位组合起来表示一个栈元素时,是不能拆分的
再回过头看,所有的dup指令,不过是根据栈元素的实际存放的类型的排列组合,梳理出来的一些复制一个或者两个栈顶元素的实际操作方式而已
就是因为他是逆向推导的,所以看起来不好理解
控制转移指令
控制转移指令可以让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)
至此
虚拟机中的指令集的大致基本设计逻辑以及意图已经基本介绍清楚了,如需要更深一步的了解,请查看虚拟机规范
转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-185-1-1.html