[三] java虚拟机 JVM字节码 指令集 bytecode 操…

2018-09-01 05:40:41来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

说明,本文的目的在于从宏观逻辑上介绍清楚绝大多数的字节码指令的含义以及分类
只要认真阅读本文必然能够对字节码指令集有所了解
如果需要了解清楚每一个指令的具体详尽用法,请参阅虚拟机规范

指令简介

计算机指令就是指挥机器工作的指示和命令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是计算机的工作过程。
通常一条指令包括两方面的内容: 操作码和操作数,操作码决定要完成的操作,操作数指参加运算的数据及其所在的单元地址。
虚拟机的字节码指令亦是如此含义
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文件只会出现数字形式的操作码
但是为了便于人识别,操作码有他对应的助记符形式
接下来所有的指令的说明,都是以助记符形式表达的
但是要明确,实际的执行运行并不存在助记符这些东西,都是根据操作码的值来执行
 
指令本身就是为了功能逻辑运算
运算自然要处理数据
所以说指令的设计是逻辑功能点与数据类型的结合
接下来先看下有哪些数据类型和逻辑功能点

数据类型

image_5b869c5d_c55
 
上一篇文章中已经说明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、iconst_m1、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...

image_5b869c5d_3bf8
对于虚拟机执行方法来说,操作数栈是工作区, 所以数据的流向是对于他  操作数栈   来说的  
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
右移有两种:
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
 

操作数栈管理指令

操作数栈管理指令,顾名思义就是直接用于管理操作栈的
对于操作数栈的直接操作主要有 出栈/复制栈顶元素 / 以及 交换栈顶元素

出栈,   分为将操作数栈栈顶的几个元素出栈,一个元素或者两个元素
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

假设栈的示意结构如下图所示,(只是给出来一种可能每个元素的类型都可能是随机的)
左边表示呈现出来的栈元素 右边是内部的线性形式  我们当做数组好了
image_5b869c5d_3c9b
对栈元素的处理,显然指的是对于栈元素内部数组的处理
所以自然要分为    
到底是直接复制一个单位的数据        
还是直接复制两个单位的数据 
 
 
一次复制占用一个单位空间   的指令 使用dup  
一次复制占用两个单位空间   的指令 使用dup2
 
一次复制占用一个单位空间 时 假设复制的栈顶是array[0] 
dup 可以理解为dup_x0    
插入到他栈顶的内部线性结构的第(1+0)个元素下面 所以array[0] 对应的必然是一个完整的栈元素 ,必然是分类1 不可能是分类2的一半!
image_5b869c5d_24b7
dup_x1                           
插入到他栈顶的内部线性结构的第(1+1)个元素下面 也就是插到第二个下面  因为array[0] 对应value1为分类1  
如果接下来的是分类2的数据,必然接下来的两个单元array[1] 和array[2]是不可分割的,也就是不可能插入到array[1] 后面,所以array[1] 对应value2 也必须是分类1 也就是两个都是分类1
image_5b869c5d_5c27
 
dup_x2                          
插入到他栈顶的内部线性结构的第(1+2)个元素下面 也就是插到第三个后面,array[0] 对应value1为分类1 为分类1  
那么接下来的两个单位array[1] 和array[2],可以是一个分类2  也可以是两个分类1,都是可以的
image_5b869c5d_3651
 
image_5b869c5d_7212
 
一次复制占用两个单位的数据类型 时
dup2 可以理解为dup2_x0   
插入到他栈顶的内部线性结构的第(2+0)个元素下面 
这一次复制的两个单位array[0] 和 array[1],  到 array[1]下面  
可能是对应value1 和value2 表示两个分类1  也可能是对应一个value1 表示类型为分类2 
image_5b869c5d_3101
image_5b869c5d_6ad0
 
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
image_5b869c5d_3201
image_5b869c5d_3543


dup2_x2  插入到他栈顶的内部线性结构的第(2+2)个元素下面 也就是复制array[0] 和 array[1] 到第四个内部元素 array[3]的下面
一次复制两个,放到第四个下面
这种情形下的组合就非常多了
全都是分类1的数据
image_5b869c5d_3cea

全部都是分类2
array[0]  和 array[1]  对应value1 表示一个分类2数据
array[2]  和 array[3]     对应value2 表示一个分类2数据
image_5b869c5d_5ddd

array[0]  和 array[1]  对应value1 表示一个分类2数据
array[2]  和 array[3]     对应value2 和 value3表示两个分类1数据
image_5b869c5d_4ebd

array[0]  和 array[1]  对应value1 和value2 表示两个分类1 数据
array[2]  和 array[3]    对应value3表示一个分类2数据
image_5b869c5d_4ea4

所以说只需要明确以下几点,就不难理解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)

 
至此,虚拟机中的指令集的大致基本设计逻辑以及意图已经基本介绍清楚了,如需要更深一步的了解,请查看虚拟机规范  

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:volatile、static

下一篇:Redis持久化