JVM 中篇(3):字节码指令集

概述

执行模型

如果不考虑异常处理的话,那么Java虚拟机的解释器可以使用下面这个伪代码当做最基本的执行模型来理解

do{
   
    自动计算PC寄存器的值加1;
    根据PC寄存器的指示位置,从字节码流中取出操作码;
    if(字节码存在操作数) 从字节码流中取出操作数;
    执行操作码所定义的操作;
}while(字节码长度>0)

字节码与数据类型

在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。

  • 例如,iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。

对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:

  • i代表对int类型的数据操作,
  • l代表long
  • s代表short
  • b代表byte
  • c代表char
  • f代表float
  • d代表double

也有一些指令的助记符中没有明确地指明操作类型的字母,

  • arraylength 指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。

还有另外一些指令,如无条件跳转指令goto则是与数据类型无关的。

大部分的指令都没有支持整数类型 byte、char和short,甚至没有任何指令支持 boolean 类型。编译器会在编译期或运行期将byte和short类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据。

  • 与之类似,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型。

指令分类

由于完全介绍和学习这些指令需要花费大量时间。为了能够更快地熟悉和了解这些基本指令,这里将JVM中的字节码指令集按用途大致分成 9类。

  • 加载与存储指令
  • 算术指令
  • 类型转换指令
  • 对象的创建与访问指令
  • 方法调用与返回指令
  • 操作数栈管理指令
  • 比较控制指令
  • 异常处理指令
  • 同步控制指令

(说在前面)在做值相关操作时:

一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,可能是对象的引用)被压入操作数栈。
一个指令,也可以从操作数栈中取出一到多个值(pop多次),完成赋值、加减乘除、方法传参、系统调用等等操作。

1.加载与存储指令

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递。

常用指令

  1. 【局部变量压栈指令】将一个局部变量加载到操作数栈:xload、xload_(其中x为i、1、f、d、a, n为0到3)
  2. 【常量入栈指令】将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、1dc2_W、aconst_null、iconst_m1、iconst_、lconst_、fconst_、dconst_
  3. 【出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:xstore、xstore_(其中x为i、l、f、d、a, n为0到3) ; xastore (其中x为i、1、f、d、a、b、C、s)
  4. 扩充局部变量表的访问索引的指令: wide

上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload_《n》)。

  • 这些指令助记符实际上代表了一组指令(例如 iload_代表了iload_0、iload_1、iload_2和iload_3这几个指令)。这几组指令都是某个带有一个操作数的通用指令(例如 iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中。
  • 除此之外,它们的语义与原生的通用指令完全一致(例如iload_0的语义与操作数为0时的iload指令语义完全一致)。在尖括号之间的字母指定了指令隐含操作数的数据类型,代表非负的整数,代表是int类型数据,代表long类型,代表float类型,代表double类型。

再谈操作数栈与局部变量表

操作栈:
在这里插入图片描述
我们知道,Java字节码是Java虚拟机所使用的指令集。因此,它与Java虚拟机基于栈的计算模型是密不可分的。。
在解释执行过程中,每当为Java方法分配栈桢时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果。

  • 具体来说便是: 执行每一条指令之前,Java虚拟机要求该指令的操作数已被压入操作数栈中。
  • 在执行指令时,Java虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中
    在这里插入图片描述

以加法指令iadd为例。假设在执行该指令前,栈顶的两个元素分别为int值1和int值2,那么iadd指令将弹出这两个int,并将求得的和int值3压入栈中。

在这里插入图片描述
由于 iadd 指令只消耗栈顶的两个元素,因此,对于离栈顶距离为2的元素,即图中的问号,iadd 指令并不关心它是否存在,更加不会对其进行修改。

局部变量表:
Java方法栈桢的另外一个重要组成部分则是局部变量区,字节码程序可以将计算的结果缓存在局部变量区之中。实际上,Java虚拟机将局部变量区当成一个数组,依次存放this指针(仅非静态方法),所传入的参数,以及字节码中的局部变量。

和操作数桟一样,long类型以及double类型的值将占据两个单元,其余类型仅占据一个单元

在这里插入图片描述
举例:

public void foo(long l,float f){
   
    {
   
        int i = 0;
    }
    {
   
        String s = "Hello,World";
    }
}

对应的图示:
在这里插入图片描述
第一个是this
第二个l是long型,占据两个槽位
第三个 f 是float型,占据一个槽位
i和s共用一个槽位

在栈帧中,与性能调优关系最为密切的部分就是局部变量表。局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

  • 在方法执行时,虚拟机使用局部变量表完成方法的传递

局部变量压栈指令

局部变量压栈指令将给定的局部变量表中的数据压入操作数栈。

iload 从局部变量中装载int类型值

lload 从局部变量中装载long类型值

fload 从局部变量中装载float类型值

dload 从局部变量中装载double类型值

aload 从局部变量中装载引用类型值(refernce)

iload_0 从局部变量0中装载int类型值

iload_1 从局部变量1中装载int类型值

iload_2 从局部变量2中装载int类型值

iload_3 从局部变量3中装载int类型值

lload_0 从局部变量0中装载long类型值

lload_1 从局部变量1中装载long类型值

lload_2 从局部变量2中装载long类型值

lload_3 从局部变量3中装载long类型值

fload_0 从局部变量0中装载float类型值

fload_1 从局部变量1中装载float类型值

fload_2 从局部变量2中装载float类型值

fload_3 从局部变量3中装载float类型值

dload_0 从局部变量0中装载double类型值

dload_1 从局部变量1中装载double类型值

dload_2 从局部变量2中装载double类型值

dload_3 从局部变量3中装载double类型值

aload_0 从局部变量0中装载引用类型值

aload_1 从局部变量1中装载引用类型值

aload_2 从局部变量2中装载引用类型值

aload_3 从局部变量3中装载引用类型值

iaload 从数组中装载int类型值

laload 从数组中装载long类型值

faload 从数组中装载float类型值

daload 从数组中装载double类型值

aaload 从数组中装载引用类型值

baload 从数组中装载byte类型或boolean类型值

caload 从数组中装载char类型值

saload 从数组中装载short类型值

xload_n xload_0 xload_1 xload_2 xload_3
iload_n iload_0 iload_1 iload_2 iload_3
lload_n lload_0 lload_1 lload_2 lload_3
fload_n fload_0 fload_1 fload_2 fload_3
dload_n dload_0 dload_1 dload_2 dload_3
aload_n aload_0 aload_1 aload_2 aload_3

这类指令大体可以分为:

  • xload_(x为i、l、f、d、a,n为0到3):指令xload_n表示将第n个局部变量压入操作数栈,比如 iload_1、fload_0、aload_0 等指令。其中aload_n表示将一个对象引用压栈。

  • xload (x为i、l、f、d、a) :指令xload通过指定参数的形式,把局部变量压入操作数栈,当使用这个命令时,表示局部变量的数量可能超过了4个,比如指令iload、fload等。
    在这里插入图片描述

常量入栈指令

常量入栈指令的功能是将常数压入操作数栈,根据数据类型和入栈内容的不同,又可以分为 const系列、push系列和ldc指令。

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压入栈

lconst_1 将long类型常量1压入栈

fconst_0 将float类型常量0压入栈

fconst_1 将float类型常量1压入栈

dconst_0 将double类型常量0压入栈

dconst_1 将double类型常量1压入栈

bipush 将一个8位带符号整数压入栈

sipush 将16位带符号整数压入栈

ldc 把常量池中的项压入栈

ldc_w 把常量池中的项压入栈(使用宽索引)

ldc2_w 把常量池中long类型或者double类型的项压入栈(使用宽索引)

指令const系列:
用于对特定的常量入栈,入栈的常量隐含在指令本身里。指令有: iconst_(i从-1到5)、lconst_(l从0到1)、fconst_(f从0到2)、dconst_(d从0到1)、aconst_null。

比如,

  • iconst_m1-1 压入操作数栈;
  • iconst_ (x为0到5)将x压入栈:
  • lconst_0、 lconst_1 分别将长整数和1压入栈;
  • fconst_0、 fconst_1、 fconst_2 分别将浮点数、1、2压入栈;
  • dconst_0 和 dconst_1 分别将 double型和1压入栈
  • aconst_nul 将 nu11 压入操作数栈;

从指令的命名上不难找出规律,

  • 指令助记符的第一个字符总是喜欢表示数据类型,
    • i表示整数,
    • 1表示长整数
    • f表示浮点数,
    • d表示双精度浮点,习惯上用a表示对象引用。

如果指令隐含操作的参数,会以下划线形式给出。

指令push系列:
主要包括 bipushsipush 。它们的区别在于接收数据类型的不同,

  • bipush接收8位整数作为参数,
  • sipush接收16位整数,
  • 它们都将参数压入栈。

指令ldc系列:

  • 如果以上指令都不能满足需求,那么可以使用万能的1dc指令,它可以接收一个8位的参数,该参数指向常量池中的int、float或者String的索引,将指定的内容压入堆栈。
  • 类似的还有1dc_w,它接收两个8位参数,能支持的索引范围大于1dc。
  • 如果要压入的元素是long或者double类型的,则使用1dc2_w指令,使用方式都是类似的。

总结:
在这里插入图片描述

例:int类型举例
在这里插入图片描述
其他类型举例
在这里插入图片描述

出栈装入局部变量表指令

出栈装入局部变量表指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值。

istore 将int类型值存入局部变量

lstore 将long类型值存入局部变量

fstore 将float类型值存入局部变量

dstore 将double类型值存入局部变量

astore 将将引用类型或returnAddress类型值存入局部变量

istore_0 将int类型值存入局部变量0

istore_1 将int类型值存入局部变量1

istore_2 将int类型值存入局部变量2

istore_3 将int类型值存入局部变量3

lstore_0 将long类型值存入局部变量0

lstore_1 将long类型值存入局部变量1

lstore_2 将long类型值存入局部变量2

lstore_3 将long类型值存入局部变量3

fstore_0 将float类型值存入局部变量0

fstore_1 将float类型值存入局部变量1

fstore_2 将float类型值存入局部变量2

fstore_3 将float类型值存入局部变量3

dstore_0 将double类型值存入局部变量0

dstore_1 将double类型值存入局部变量1

dstore_2 将double类型值存入局部变量2

dstore_3 将double类型值存入局部变量3

astore_0 将引用类型或returnAddress类型值存入局部变量0

astore_1 将引用类型或returnAddress类型值存入局部变量1

astore_2 将引用类型或returnAddress类型值存入局部变量2

astore_3 将引用类型或returnAddress类型值存入局部变量3

iastore 将int类型值存入数组中

lastore 将long类型值存入数组中

fastore 将float类型值存入数组中

dastore 将double类型值存入数组中

aastore 将引用类型值存入数组中

bastore 将byte类型或者boolean类型值存入数组中

castore 将char类型值存入数组中

sastore 将short类型值存入数组中

wide指令

wide 使用附加字节扩展局部变量索引

xstore_n xstore_0 xstore_1 xstore_2 xstore_3
istore_n istore_0 istore_1 istore_2 istore_3
lstore_n lstore_0 lstore_1 lstore_2 lstore_3
fstore_n fstore_0 fstore_1 fstore_2 fstore_3
dstore_n dstore_0 dstore_1 dstore_2 dstore_3
astore_n astore_0 astore_1 astore_2 astore_3
  • 这类指令主要以 store 的形式存在,比如 xstore (x为i、1、f、d、a)、xstore_n(x为i、1、f、d、a,n为0至3)。
  • 其中,指令istore_n将从操作数栈中弹出一个整数,并把它赋值给局部变量索引 n 位置。
  • 指令xstore由于没有隐含参数信息,故需要提供一个byte类型的参数类指定目标局部变量表的位置。

说明
一般说来,类似像store这样的命令需要带一个参数,用来指明将弹出的元素放在局部变量表的第几个位置。

  • 但是,为了尽可能压缩指令大小,使用专门的istore_1指令表示将弹出的元素放置在局部变量表第1个位置。类似的还有istore_0、istore_2、istore_3,它们分别表示从操作数栈顶弹出一个元素,存放在局部变量表第0、2、3个位置。
  • 由于局部变量表前几个位置总是非常常用,因此这种做法虽然增加了指令数量,但是可以大大压缩生成的字节码的体积。
  • 如果局部变量表很大,需要存储的槽位大于3,那么可以使用istore指令,外加一个参数,用来表示需要存放的槽位位置。
    在这里插入图片描述

foo方法解析

  • 操作数栈最大深度为5
  • 两个代码块共用了一个槽位,在第一个代码块执行完毕之后第二个代码块会复用第一个代码块的槽位

如图所示:
在这里插入图片描述

2. 算术指令

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

整数运算

iadd 执行int类型的加法

ladd 执行long类型的加法

isub 执行int类型的减法

lsub 执行long类型的减法

imul 执行int类型的乘法

lmul 执行long类型的乘法

idiv 执行int类型的除法

ldiv 执行long类型的除法

irem 计算int类型除法的余数

lrem 计算long类型除法的余数

ineg 对一个int类型值进行取反操作

lneg 对一个long类型值进行取反操作

iinc 把一个常量值加到一个int类型的局部变量上

逻辑运算

移位操作

ishl 执行int类型的向左移位操作

lshl 执行

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值