jvm之字节码指令集解读(上)

目录

概述

执行模型

指令分析

加载与存储指令

作用

常用指令

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

局部变量压栈指令 

 局部变量压栈常用指令集

局部变量压栈指令剖析

举个例子

 常量入栈指令


概述

Java字节码指令集是一组计算机指令,用于在Java虚拟机上执行Java程序。这些指令编码了操作码、操作数和控制信息,可以用于执行Java语言程序的所有操作,如变量赋值、方法调用、控制流与异常处理等。Java字节码指令集可以直接被Java虚拟机读取和解释,并且保证了Java程序在不同平台上的可移植性。

执行模型

如果不考虑异常处理的话,那么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多次),完成赋值、加减乘除、方法传参、系统调用等等操作。

加载与存储指令

作用

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

常用指令

【局部变量压栈指令】将一个局部变量加载到操作数栈:xload、xload_<n>(其中x为i、l、f、d、a,n为0到3)

【常量入栈指令】将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>)、fconst_<f>、dconst_<d>

【出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:xstore、xstore_<n>(其中x为i、l、f、d、a,n为0到3);xastore(其中x为i、l、f、d、a、b、c、s)

扩充局部变量表的访问索引的指令:wide

上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload_<n>)。这些指令助记符实际上代表了一组指令(例如iload_<n>代表了iload_0、iload_1、iload_2iload_3这几个指令)这几组指令都是某个带有一个操作数的通用指令(例如iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中。

除此之外,它们的语义与原生的通用指令完全一致(例如iload_0的语义与操作数为0时的iload指令语义完全一致)。在尖括号之间的字母指定了指令隐含操作数的数据类型,<n>代表非负的整数,<i>代表是int类型数据,<l>代表long类型,<f>代表float类型,<d>代表double类型。

操作byte、char、short和boolean类型数据时,经常用int类型的指令来表示。

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

操作数栈(Operand Stacks)

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

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

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

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

局部变量表(Local Variables)

Java方法栈桢的另外一个重要组成部分则是局部变量区,字节码程序可以将计算的结果缓存在局部变量区之中。

实际上,Java虚拟机将局部变量区当成一个数组,依次存放this指针(仅非静态方法,即0位放的是this),所传入的参数,以及字节码中的局部变量。

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

 

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

 this表示当前类的引用,l和f的类型的值占两个槽位,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_nxload_0xload_1xload_2xload_3
iload_niload_0iload_1iload_2iload_3
lload_nlload_0lload_1lload_2lload_3
fload_nfload_0fload_1fload_2fload_3
dload_ndload_0dload_1dload_2dload_3
aload_naload_0aload_1aload_2aload_3

局部变量压栈指令剖析

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

这类指令大体可以分为:

  • xload_<n>(x为i、l、f、d、a,n为0到3)
  • xload(x为i、l、f、d、a

说明:在这里,x的取值表示数据类型。

指令xload_n表示将第n个局部变量压入操作数栈,比如iload_1、fload_0、aload_0等指令。其中aload_n表示将一个对象引用压栈。

指令xload通过指定参数的形式,把局部变量压入操作数栈,当使用这个命令时,表示局部变量的数量可能超过了4个,比如指令iload、fload等。

举个例子

public void load(int num, Object obj, long count, boolean flag, short[] arr) {
    System.out.println(num);
    System.out.println(obj);
    System.out.println(count);
    System.out.println(flag);
    System.out.println(arr);
}

字节码执行过程: 

 常量入栈指令

指令含义
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类型的项压入栈(使用宽索引)

常量入栈常用指令集

xconst_n范围xconst_nullxconst_m1xconst_0xconst_1xconst_2xconst_3xconst_4xconst_5
iconst_n[-1, 5]iconst_m1iconst_0iconst_1iconst_2iconst_3iconst_4iconst_5
lconst_n0, 1lconst_0lconst_1
fconst_n0, 1, 2fconst_0fconst_1fconst_2
dconst_n0, 1dconst_0dconst_1
aconst_nnull, String literal, Class literalaconst_null
bipush一个字节,2^8^,[-2^7^, 2^7^ - 1],即[-128, 127]
sipush两个字节,2^16^,[-2^15^, 2^15^ - 1],即[-32768, 32767]
ldc四个字节,2^32^,[-2^31^, 2^31^ - 1]
ldc_w宽索引
ldc2_w宽索引,long或double

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个风轻云淡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值