关于指令:我所知道JVM虚拟机之字节码指令集与解析一

2次阅读

共计 5963 个字符,预计需要花费 15 分钟才能阅读完成。

前言

后面咱们解说了 Class 文件的构造、以及采纳不同的形式来解读 Class 文件

  • 第一种是采纳字节码一行一行的解读、
  • 第二种是采纳 javap 的指令进行解读、
  • 第三应用第三方(idea、Eclipse)插件进解读、

然而针对于 Class 文件里办法的字节码指令,咱们并没有进行细节的指令剖析

本篇文章,咱们开始对字节码指令进行剖析,看看示例代码里的办法到底做了什么事件?

一、指令概述


Java 字节码对于虚拟机,就如同汇编语言对于计算机,属于根本执行指令。

Java 虚拟机的指令由 一个字节长度 的、代表着 某种特定操作含意的数字 (称为操作码,Opcode) 以及 追随其后的零至多个代表此操作所需参数 (称为操作数,Operands) 而形成。

因为 Java 虚拟机采纳面向操作数栈而不是寄存器的构造,所以大多数的指令都不蕴含操作数

咱们能够采纳上一篇文章的示例代码与字节码剖析进行解析看看

咱们依据上篇的思路,找找这些字节码指令对应的字节码是什么呢?代表什么意思呢?

虚拟机限度了 Java 操作码的长度为一个字节(即 0~255),这意味着操作码总数不可能超过 256 条

官网文档: https: //docs.oracle.com/javase/specs/jvms/se8/htm1/jvms-6.html

相熟虚拟机的指令对于 动静字节码生成、反编译 Class 文件、Class 文件修补都有着十分重要的价值。因而浏览学节码作为理解 ava 虚拟机的根底技能,须要熟练掌握常见指令。

执行模型

================================

如果不思考异样解决的话

那么 Java 虚拟机的解释器能够应用上面这个伪代码当做最根本的执行模型来了解

do{

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

字节码与数据类型

================================

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

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

咱们能够依据上篇的示例代码进行解析剖析看看

咱们能够看看局部变量表里的索引:0 的值是什么?

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

  • 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 类型作为运算类型

 byte b1 = 12;
 short s1 = 10
 int i = b1 + s1

指令的分类

================================

因为齐全介绍和学习这些指令须要破费大量工夫。为了让大家可能更快地相熟和理解这些根本指令,这里将 JVN 中的字节码指令集按用处大抵分成 9 类

  • 加载与存储指令
  • 算术指令
  • 类型转换指令
  • 对象的创立与拜访指令
  • 办法调用与返回指令
  • 操作数栈治理指令
  • 比拟控制指令
  • 异样解决指令
  • 同步控制指令

写在后面的,对于这些不同分类指令,大多在做值相干操作时:

一个指令能够从局部变量表、常量池、堆中对象、办法调用、零碎调用中等获得数据,这些数据(可能是值,可能是对象的援用)被压入操作数栈。

一个指令也能够从操作数栈中取出一到多个值(pop 屡次),实现赋值、加减乘除、办法传参、零碎调用等等操作。

二、加载与存储指令概述


作用

================================

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

常见指令

================================

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

指令助记符实际上代表了一组指令

例如:iload_<n>代表了 iload_0、iload_1、iload_2、iload_3 这几个指令。

这几组指令都是某个带有一个操作数的通用指令(例如:iload)的非凡模式,对于这若干组非凡指令来说,它们外表上没有操作数,不须要进行取操作数的动作,但操作数都隐含在指令中

除此之外它们的语义与原生的通用指令完全一致

例如 iload_0的语义与操作数为时的 iload 指令语义完全一致。

示例举例:

iload_0: 将局部变量表中索引为 0 地位上的数据压入操作数栈中,这是占一个字节

iload 0: 将局部变量表中索引为 0 地位上的数据压入操作数栈中,这是占两个字节

在尖括号之间的字母指定了指令隐含操作数的数据类型,具体信息如下:

  • <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 指令只耗费栈顶的两个元素,因而,对于离栈顶间隔为? 的元素,即图中的问号,iadd 指令并不关怀它是否存在,更加不会对其进行批改。

局部变量表(Local Variables)

================================

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

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

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

举例:
public vid foo(long l,fl1oatf) {
    {int i = 0;}
    {string s = "He11o, wor1d" ;}
}

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

在办法执行时,虚拟机应用局部变量表实现办法的传递

四、加载与存储指令的局部变量入栈指令


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

这类指令大体能够分为:

  • 指令信息: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_e 等指令。其中 aload_n 示意 将个对象援用压栈

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

接下来应用示例代码来演示一下局部变量压栈指令

public class LoadAndStoreTest {

    //1. 局部变量压栈指令
    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.print1n(flag);
        system.out.println(arr);
    }
}

咱们应用编译一下,并且在 idea 中应用插件来看看该办法具体的指令有哪些?

此时咱们依据这些指令进行剖析看看,并且看看局部变量表与操作数栈是怎么样的状况


咱们也能够应用 idea 的插件校验一下,看看是否办法里的局部变量表统一?

接下来咱们剖析一下指令是怎么操作局部变量表与操作数栈的,


当咱们操作局部变量表索引为:5 的时候,就会发现它占用了两个字节:iload 5,why?

五、、加载与存储指令的常量入栈指令


常量入栈指令的性能是

常数压入操作数栈,依据数据类型和入栈内容的不同,又能够分为 const 系列、push 系列和 ldc 指令。

const 系列

================================

用于对特定的常量入栈,入栈的常量隐含在指令自身里。

指令有: iconst_<i>、形容: i 从 - 1 到 5
指令有: lconst_<l>、形容: l 从 0 到 1
指令有: fconst_<f>、形容: f 从 0 到 2
指令有: dconst_<d>、形容: d 从 0 到 1
指令有: aconst_null、形容: d 从 0 到 1

比方有示例:

  • iconst_m1,形容:将 - 1 压入操作数栈;
  • iconst_x (x 为 0 到 5)将 x 压入栈:
  • lconst_0、lconst_1 别离将长整数 0 和 1 压入栈;
  • fconst_0、fconst_1、fconst_2 别离将浮点数 0、1、2 压入栈;
  • dconst_0、dconst_1 别离将 double 型 0 和 1 压入栈。
  • aconst_null 将 null 压入操作数栈;

从指令的命名上不难找出法则,指令助记符的第一个字符总是喜爱示意数据类型。

  • i 示意整数、l 示意长整数
  • f 示意浮点数、d 示意双精度浮点
  • a 示意对象援用

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

push 系列

================================

次要包含 bipush 和 sipush,它们区别在于 接收数据类型的不同:

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

指令 ldc 系列

================================

如果以上指令都不能满足需要,那么能够应用万能的 ldc 指令,它能够 接管一个 8 位的参数,该参数指向常量池中的 int、float 或者 String 的索引,将指定的内容压入堆栈

相似的还有 ldc_w,它接管两个 8 位参数,能反对的索引范畴大于 ldc。
如果要 压入的元素是 1ong 或者 double 类型的, 则应用 1dc2_w 指令,应用形式都是相似的。

接下来应用示例代码来演示一下常量压栈指令

public class LoadAndStoreTest {

    //2. 常量入栈指令
    public void pushConstLdc() {
        int i = 1;
        int a = 5;
        int b = 6;
        int c = 127;
        int d = 128;
        int e = 32767;
        int f = 32768;
    }
}

咱们应用编译一下,并且在 idea 中应用插件来看看该办法具体的指令有哪些?

尽管咱们都是 int 类型的变量,然而指令里也有 byte、long、short 这些类型

所以咱们能够总结一下,具体类型的范畴具体定义,能够看如下图:

那么对于 float、long 类型,咱们也进行示例代码看看具体是怎么样的?

public class LoadAndStoreTest {

    //2. 常量入栈指令
    public void constLdc() {
        1ong a1 = 1;
        long a2 = 2;
        float b1 = 2;
        f1oat b2 = 3;
        double c1 = 1;
        double c2 = 2;
        Date d = null;
    }
}

咱们应用编译一下,并且在 idea 中应用插件来看看该办法具体的指令有哪些?

咱们后面也提到过压入的元素是 1ong 或者 double 类型的, 则应用 ldc2_w 指令

当咱们超出 float 类型的范畴同样也是应用 ldc2_w 的指令

六、加载与存储指令的出栈指令


出栈装入局部变量表指令

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

这类指令次要以 store 的模式存在

指令:xstore,形容: x 为 i、l、f、d、a
指令:xstore_n,形容:x 为 i、l、f、d、a,n 为至 3

其中指令 istore_n 将从操作数栈中弹出一个整数,并把它 赋值给局部变量索引 n 地位

指令 xstore 因为没有隐含参数信息,故须要提供一个 byte 类型的参数类指定指标局部变量表的地位

接下来应用示例代码来演示一下常量压栈指令

public class LoadAndStoreTest {

    //3. 出栈装入局部变量表指令
    public void store(int k,double d){
        int m = k + 2;
        long l = 12;
        string str = "atguigu";
        float f = 10.0F;
        d = 10;
    }
}

咱们应用编译一下,并且在 idea 中应用插件来看看该办法具体的指令有哪些?

此时咱们依据这些指令进行剖析看看,并且看看出栈指令是怎么样的状况



接下来应用示例代码来演示一下其余的状况阐明

public class LoadAndStoreTest {

    //4. 出栈装入局部变量表指令
    public void foo(1ong l,f1oat f){
        {int i = 0;}
        {string s = "He1lo,wor1d"}
    }
}

咱们应用编译一下,并且在 idea 中应用插件来看看该办法具体的指令有哪些?

仅接着咱们来看看局部变量表里有什么呢?

然而咱们的局部变量表长度是多少呢?咱们一起来看看

此时咱们依据这些指令进行剖析看看,并且看看出栈指令是怎么样的状况

正文完
 0