JVM_19_字节码指令集与解析举例

1.概述

  • Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令。
  • Java虚拟机的指令由 一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是面向寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码
  • 由于限制了Java虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不可能超过256条
  • 官方文档: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html
  • 熟悉虚拟机的指令对于动态字节码生成、反编译 Class 文件、 Class 文件修补都有着非常重要的价值。因此,阅读字节码作为了解 Java 虚拟机的基础技能,需要熟练掌握常见指令

1.1 执行模型

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

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

1.2 字节码与数据类型

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

1.3 指令分类

将JVM中的字节码指令集按用途大致分成9类:

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

在做值相关操作时:

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

2.加载与存储指令

2.1 作用

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

2.2 常用指令

在这里插入图片描述

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

  • 操作数占俩个字节,使用操作码iload_0 (0-3用的最多)可以节省空间只占一个字节,iload 0 (操作码 操作数)占3个字节
    在这里插入图片描述

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

在这里插入图片描述
操作数栈(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指针(仅非静态方法),所传入的参数,以及字节码中的局部变量。
  • 和操作数栈一样,long类型以及double类型的值将占据两个单元,其余类型仅占据一个单元。
    在这里插入图片描述
    举例:
public void foo(long l, float f) {
    {
        int i = e;
    }
    {
        String s = "Hello, World";
    }
}

对应的图示:
在这里插入图片描述

  • this表示当前类的引用,long 8个字节占俩个槽位,float 4个字节占一个槽位,i和s变量由于分别在各自代码块中,没有共同的生命周期,所以占同一个槽位(即槽位复用)
  • 在栈帧中,与性能调优关系最为密切的部分就是局部变量表。局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
  • 在方法执行时,虚拟机使用局部变量表完成方法的传递

2.4 局部变量压栈指令

  • 局部变量压栈指令将给定的局部变量表中的数据压入操作数栈
    在这里插入图片描述
    举例:
public class LoadAndStoreTest {

    // 局部变量压栈指令
    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);
    }
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2.5 常量入栈指令

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

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

2.6 出栈装入局部变量表指令

在这里插入图片描述

举例:

在这里插入图片描述

3.算术指令

3.1 概述

在这里插入图片描述
在这里插入图片描述

3.2 所有算术指令

在这里插入图片描述

3.3 关于++操作

public class IAdd {
    public void m1() {
        int i = 10;
        i++;
    }

    public void m2() {
        int i = 10;
        ++i;
    }

    public void m3() {
        int i = 10;
        int a = i++;

        int j = 20;
        int b = ++j;
    }

    public void m4() {
        int i = 10;
        i = i++;
        System.out.println(i);
    }
}
  • 对于不参与运算的情况下,i++ 和 ++i 的字节码操作是一样的
    在这里插入图片描述
    在这里插入图片描述

  • 参与运算的情况下
    在这里插入图片描述
    在这里插入图片描述

3.4 比较指的令说明

在这里插入图片描述

4.类型转换指令

4.1 类型转换指令说明

  • 类型转换指令可以将两种不同的数值类型进行相互转换。
  • 这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

4.2 宽化类型转换指令

在这里插入图片描述
在这里插入图片描述

4.3 窄化类型转换指令

在这里插入图片描述
在这里插入图片描述

5.对象的创建与访问指令

Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支持。有一系列指令专门用于对象操作,可进一步细分为创建指令、字段访问指令、数组操作指令、类型检查指令。

5.1 创建指令

在这里插入图片描述

5.2 字段访问指令

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.3 数组操作指令

在这里插入图片描述
在这里插入图片描述

5.4 类型检查指令

在这里插入图片描述

6.方法调用与返回指令

6.1 方法调用指令

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6.2 方法返回指令

在这里插入图片描述
在这里插入图片描述

7.操作数栈管理指令

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

8.控制转移指令

程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为比较指令(在之前的算术指令)、条件跳转指令、比较条件跳转指令、多条件分支跳转指令、无条件跳转指令等

8.1 条件跳转指令

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

8.2 比较条件跳转指令

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

8.3 多条件分支跳转指令

在这里插入图片描述
指令tableswitch的示意图如下图所示。由于tableswitch的case值是连续的,因此只需要记录最低值和最高值,以及每一项对应的offset偏移量,根据给定的index值通过简单的计算即可直接定位到offset。
在这里插入图片描述
指令lookupswitch处理的是离散的case值,但是出于效率考虑,将case-offset对按照case值大小排序,给定index时,需要查找与index相等的case,获得其offset,如果找不到则跳转到default。指令lookupswitch如下图所示。
在这里插入图片描述
举例:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

8.4 无条件跳转指令

在这里插入图片描述

9.异常处理指令

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

在这里插入图片描述

在finally执行前return的值已经决定好的,开发者这么设计的原因应该是为了让try catch finally结构尽量只跟异常有关

10.同步控制指令

Java虚拟机支持两种同步结构:方法级的同步和方法内部一段指令序列的同步,这两种同步都是使用monitor来支持的

10.1 方法级的同步

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

10.2 方法内指定指令序列的同步

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须执行其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束。

为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit指令

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值