第二章:字节码指令集与解析案例

news2024/11/28 14:37:47

    • 一、概述
      • 执行模型
      • 字节码与数据类型
      • 字节码指令分类
        • 加载与存储指令
          • 局部变量压栈指令
          • 常量入栈指令
          • 出栈装入局部变量表指令
        • 算术运算指令
          • 代码举例一
          • 代码举例二
          • 代码举例三:++i 和 i++ 的区别
          • 比较指令的说明
        • 类型转换指令
          • 宽化类型转换(Widening Numeric Conversions)
          • 窄化类型转换(Narrowing Numeric Conversion)
        • 对象的创建与访问指令
          • 创建指令
          • 字段访问指令
          • 数组操作指令
          • 类型检查指令
        • 方法调用与返回指令
          • 方法调用指令
          • 方法返回指令:
        • 操作数管理指令
        • 控制转移指令
          • 条件转移指令
          • 比较条件转移指令
          • 多条件分支跳转指令
          • 无条件跳转指令
        • 抛出异常指令
        • 异常处理与异常表
        • 同步控制指令
          • 方法级的同步:是隐式的
          • 同步一段指令集序列

一、概述

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

字节码指令分类

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

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

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

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

加载与存储指令

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

常用指令

1、[局部变量压栈指令] 将一-个局部变量加载到操作数栈: xload、 xload_ ( 其中x为i、l、f、d、a,n为0到3)
2、[常量入栈指令] 将-个常量加载到操作数栈: bipush、 sipushldc、ldc_ W、ldc2_ W、aconst_ null、 iconst_ m1、 iconst 、 lconst_ 、 fconst_ 、 dconst_ 、

3、[ 出栈装入局部变量表指令]将个数值从操作数栈存储到局部变量表: xstore、xstore_ (其中x为i、1、f、d、a,n为0到3);xastore(其中x为i、1、f、d、a、b、C、s)
4、扩充局部变量表的访问索引的指令: wide。

只要看见 load、push、ldc、const 就是将数据压入操作数栈

看见 store 就是出栈加载到局部变量表中

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

这些指令助记符实际上代表了一组指令==(例如iload_ 代表 了 iload_ 0、iload_ 1、iload 2和iload 3这几个指令)==。这几组指令都是某个带有一个操作数的通用指令(例如iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数
的动作,但操作数都隐含在指令中。

例如:

iload_0 : 将局部变量表索引为0 的数据压入操作数栈中

iload 0 俩组指令意思一样,但是 前者占一个字节,后者占 三个字节。

局部变量压栈指令

代码演示:

    //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.println(flag);
        System.out.println(arr);
    }

操作数栈和局部变量表初始状态:

image-20221019222106792

执行完 load 指令:

image-20221019222641888

  1. load_n : 范围只到 0 ~ 3 ,如果还有操作数使用 load 4 的形式。
  2. byte、char、short、boolean 类型参数最终都会转成 int 类型
常量入栈指令

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

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

比如:

  • 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_ nu1l将nu11压 入操作数栈;

从指令的命名上不难找出规律,指令助记符的第-一个字符总是喜欢表示数据类型,i表示整数,l 表示长整数,f表示浮点数,d表示双精度浮点,习惯上用a表示对象引用。如果指令隐含操作的参数,会以下划线形式给出。

指令push系列:主要包括bipush和sipush。它们的区别在于接收数据类型的不同,bipush接收8位 整数作为参数,sipush接收16位整数,它们都将参数压入栈。

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

类似的还有1dc_ W,它接收两个8位参数,能支持的索引范围大于1dc。
如果要压入的元素是1ong或者double类型的,则使用1dc2_ _w指令,使用方式都是类似的。

image-20221021084241918

代码演示:

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

字节码指令:

 0 bipush -8
 2 istore_1
 3 iconst_m1
 4 istore_2
 5 iconst_5
 6 istore_3
 7 bipush 6
 9 istore 4
11 bipush 127
13 istore 5
15 sipush 128
18 istore 6
20 sipush 32767
23 istore 7
25 ldc #7 <32768>
27 istore 8
29 return
出栈装入局部变量表指令

出栈装入局部变量表指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值。这类指令主要以store的形式存在,比如xstore (x为i、 1、f、d、a)、xstore_ n (x为i、l、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指令,外加一个参数,用来表示需要存放的槽位位置

例如:
istore 4 : 弹出栈顶元素保存到局部变量表索引为4的位置上

算术运算指令

1、作用

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

2、分类

大体上算术指令可以分为两种:对整型数据进行运算的指令与对浮点类型数据进行运算的指令。

3、byte、short、 char和boolean类型 说明

在每一大类中,都有针对Java虚拟机具体数据类型的专用算术指令。但没有直接支持byte、short、char和boolean类型的算术指令,对于这些数据的运算,都使用 int类型的指令来处理。此外,在处理boolean、byte、 short 和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。

4、运算时的溢出

数据运算可能会导致溢出,例如两个很大的正整数相加,结果可能是一个负数。其实Java虚拟机规范并无明确规定过整型数据溢出的具体结果,仅规定了在处理整型数据时,只有除法指令以及求余指令中当出现除数为0时会导致虚拟机抛出异常 ArithmeticException.

5、运算模式

  • 向最接近数舍入模式: JVM要求在进行浮点数计算时,所有的运算结果都必须舍入到适当的精度,非精确结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值一样接近,将优先选择最低有效位为零的;
  • 向零舍入模式:将浮点数转换为整数时,采用该模式,该模式将在目标数值类型中选择一-个最接近但是不大于原值的数字作为最精确的舍入结果;

6、NaN值使用
当一个操作产生溢出时,将会使用有符号的无穷大表示,如果某个操作结果没有明确的数学定义的话,将会使用NaN值来表示。而且所有使用NaN值作为操作数的算术操作,结果都会返回NaN;

7、所有的运算指令

加法指令: iadd、ladd、fadd、dadd
减法指令: isub、 lsub、fsub、dsub
乘法指令: imul、lmul、fmul、dmul
除法指令: idiv、ldiv、fdiv、ddiv
求余指令: irem、lrem、frem、drem //remainder:余数
取反指令: ineg、lneg、fneg、dneg //negation:取反
自增指令: iinc

位运算指令,又可分为:

  • 位移指令: ishl、 ishr、 iushr、lshl、 lshr、lushr
  • 按位或指令: ior、 lor
  • 按位与指令: iand、 land
  • 按位异或指令: ixor、 lxor

比较指令: dcmpg、dcmpl、fcmpg、fcmpl、lcmp

代码举例一
    public void method2(){
        float i = 10;
        float j = -i;
        i = -j;
    }

字节码指令:

0 ldc #4 <10.0>    // 入栈 10.0
2 fstore_1			// 弹出10.0 放入局部变量表索引为1的位置
3 fload_1			// 局部变量表索引为1的变量入栈 10.0
4 fneg				// 对栈顶元素取反并保存在栈中  -10.0
5 fstore_2          // 弹出栈顶元素放入局部变量表索引为2的位置 -10.0
6 fload_2			// 局部变量表索引为2的变量入栈   -10.0
7 fneg				// 对栈顶元素取反并保存在栈中  10.0
8 fstore_1          // 弹出栈顶元素放入局部变量表索引为1的位置 -10.0
9 return
代码举例二
    public void method3(int j){
        int i = 100;
       i = i + 10;
//         i += 10;
    }

i = i + 10; 字节码指令:

0 bipush 100
2 istore_2
3 iload_2
4 bipush 10
6 iadd
7 istore_2
8 return

i += 10; 字节码指令:

0 bipush 100
2 istore_2
3 iinc 2 by 10
6 return

i += 10; 与 i = i + 10; 的区别:

前者直接操作局部变量表中的变量,无需执行出栈入栈指令

后者有一个入栈并相加的指令

代码举例三:++i 和 i++ 的区别

代码一

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

字节码指令:

0 bipush 10
2 istore_1
3 iinc 1 by 1
6 return

当 没有变量接收时,i++ 和 ++ i 其实是一样的。字节码指令也都一样。

代码二

    public void method8(){
        int i = 10;
        i = i++;
        // i++;
        System.out.println(i);//10
    }

字节码指令:

 0 bipush 10
 2 istore_1
 3 iload_1
 4 iinc 1 by 1
 7 istore_1
 8 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
11 iload_1
12 invokevirtual #5 <java/io/PrintStream.println : (I)V>
15 return

最终局部变量表中 i 的值为10 :

image-20221021104732903

如果把上面的 i = i++, 换成 i = ++i

输出结果: 11

字节码指令:

 0 bipush 10
 2 istore_1
 3 iinc 1 by 1
 6 iload_1
 7 istore_1
 8 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
11 iload_1
12 invokevirtual #5 <java/io/PrintStream.println : (I)V>
15 return

最终局部变量表中 i 的值为 11 :

image-20221021104711574

无论是 i++ 还是 ++i ,如果没有变量接收的情况下,字节码都一样。

如果有变量接收的情况下,i++ 先引用后增加,++i 先增加后引用。

比较指令的说明

比较指 令的作用是比较栈顶两个元素的大小, 并将比较结果入栈。

比较指令有: dcmpg, dcmpl、 fcmpg、 fcmpl、 lcmp.

  • 与前面讲解的指令类似,首字符d表示double类型,f表示float , 1表示long。
  • 对于double和float类型的数字, 由于NaN的存在,各有两个版本的比较指令。以float为例, 有fcmpg和fcmp1两个指令,它们的区别在于在数字比较时,若遇到NaN值,处理结果不同。,指令dcmpl和dcmpg也是类似的,根据其命名可以推测其含义,在此不再赘述。
  • 指令lcmp针对long型整数, 由于long型整数没有NaN值, 故无需准备两套指令。

举例:

  • 指令fcmpg和 fcmpl 都从栈中弹出两个操作数,并将它们做比较,设栈顶的元素为v2,栈顶顺位第2位的元素为v1,若v1=v2,则压入0;若v1>v2则压入1; 若v1<v2则压入-1。
  • 两个指令的不同之处在于,如果遇到NaN值,fcmpg会 压入1,而fcmp1会压入-1。

image-20221021182218791

类型转换指令

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

宽化类型转换(Widening Numeric Conversions)

1、转换规则:

Java虚拟机直接支持以下数值的宽化类型转换(widening numeric conversion,小范围类型向大范围类型的安全转换)。也就是说,并不需要指令执行,包括:

  • 从int类型到long、float或者double类型。 对应的指令为: i21、i2f、 i2d
  • 从long类 型到float、double类型。 对应的指令为: 12f、 12d
  • 从float类型到double类型。对应的指令为: f2d

简化为: int --> long --> float --> double

2、精度损失问题

  • 宽化类型转换是不会因为超过目标类型最大值而丢失信息的,例如,从int转换到long, 或者从int转换到double,都不会丢失任何信息,转换前后的值是精确相等的。

  • 从int、long 类型数值转换到float,或者long类型数值转换到double时,将可能发生精度丢失一一可 能丢失掉几个最低有效位上的值,转换后的浮点数值是根据IEEE754最接近舍入模式所得到的正确整数值。尽管宽化类型转换实际上是可能发生精度丢失的,但是这种转换永远不会导致Java虚拟机抛出运行时异常。

int 、float 占用4个字节

long 、double 占用 8个字节

小——》大不会造成精度损失

大 ——》 小 会造成精度损失

3、补充说明

从byte、char和short类型到int类型的宽化类型转换实际上是不存在的。在 JVM 内部已经将这三种类型当做 int 类型来处理了,例如: byte 转换成 long,其实内部字节码指令时:i2l , 这种处理方式有两个特点:

  • 一方面可以减少实际的数据类型,如果为short和byte都准备一套指令, 那么指令的数量就会大增,而虚拟机目前的设计上,只愿意使用一个字节表示指令,因此指令总数不能超过256个,为了节省指令资源,将short 和byte当做int处理也在情理之中

  • 另一方面,由于局部变量表中的槽位固定为32位,无论是byte或者short存入局部变量表,都会占用32位空间。从这个角度说,也没有必要特意区分这几种数据类型。

窄化类型转换(Narrowing Numeric Conversion)

1、转换规则

Java虚拟机也直接支持以下窄化类型转换:

  • 从int类型至byte、short或者char类型。对应的指令有: i2b、i2c、i2s
  • 从long类型到int类型。对应的指令有: 12i
  • 从float类型到int或者long类型。对应的指令有: f2i、f2l
  • 从double类型到int、long或者float类型。对应的指令有:d2i、d21、d2f

从 long、float、double 类型转换成 byte、char、short 类型: 先转换成 int ,在具体转换成 byte、char 或者 short

long、float、double —— > int ——> byte、char、short

2、精度损失问题

窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级,因此,转换过程很可能会导致数值丢失精度。尽管数据类型窄化转换可能会发生上限溢出、下限溢出和精度丢失等情况,但是Java虛拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常

3、补充说明

当将一个浮点值窄化转换为整数类型T (T限于int或long类型之一)的时候,将遵循以下转换规则

  • 如果浮点值是NaN,那转换结果就是int或long类型的0。
  • 如果浮点值不是无穷大的话,并且在 T 类型(int、long)表示范围之内就直接四舍五入取整。如果是无穷大,取 T 类型范围的最大值。
 @Test
    public void downCast5(){
        // Not a Number
        double d1 = Double.NaN;
        int i = (int)d1;
        System.out.println(i); // 0

        // 无穷大
        double d2 = Double.POSITIVE_INFINITY;
        long l = (long)d2;
        int j = (int)d2;
        
        System.out.println(l);
        System.out.println("long类型所能表示的最大值:" + Long.MAX_VALUE); // 9223372036854775807
        System.out.println(j);
        System.out.println("int类型所能表示的最大值:" + Integer.MAX_VALUE); // 2147483647

        float f = (float)d2;
        System.out.println(f);

        float f1 = (float)d1;
        System.out.println(f1);
    }

对象的创建与访问指令

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

创建指令

虽然类实例和数组都是对象,但Java 虚拟机对类实例和数组的创建与操作使用了不同的字节码指令:

1、创建类实例的指令:

  • 创建类实例的指令:new
  • 它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈

2、创建数组的指令:

  • 创建数组的指令: newarray、anewarray、multianewarray
  • newarray:创建基本类型数组
  • anewarray:创建引用类型数组
  • multianewarray:创建多维数组

上述创建指令可以用于创建对象或者数组,由于对象和数组在Java中的广泛使用,这些指令的使用频率也非常高。

代码举例一:创建类实例对象

    //1.创建指令
    public void newInstance() {
        Object obj = new Object();
        File file = new File("atguigu.avi");
    }

字节码信息:

 0 new #2 <java/lang/Object>
 3 dup	
 4 invokespecial #1 <java/lang/Object.<init> : ()V>
 7 astore_1
 8 new #3 <java/io/File>
11 dup
12 ldc #4 <atguigu.avi>
14 invokespecial #5 <java/io/File.<init> : (Ljava/lang/String;)V>
17 astore_2
18 return

dup 复制指令,复制一份对象保存在栈中

image-20221021121135751

**代码举例二:**创建数组对象

   public void newArray() {
        int[] intArray = new int[10];
        Object[] objArray = new Object[10];
        int[][] mintArray = new int[10][10];

        String[][] strArray = new String[10][];
    }

字节码指令:

 0 bipush 10
 2 newarray 10 (int)
 4 astore_1
 5 bipush 10
 7 anewarray #2 <java/lang/Object>
10 astore_2
11 bipush 10
13 bipush 10
15 multianewarray #6 <[[I> dim 2
19 astore_3
20 bipush 10
22 anewarray #7 <[Ljava/lang/String;>
25 astore 4
27 return

String[][] strArray = new String[10][];

在字节码中将 strArray 看做了一维数组,在初始化时,只开辟了容量为10的大小。

字段访问指令

对象创建后,就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素。

  • 访问类字段(static字段,或者称为类变量)的指令:getstatic、 putstatic
  • 访问类实例字段(非static字段,或者称为实例变量)的指令: getfield、 putfield

get 压栈

put 弹栈

数组操作指令

数组操作指令主要有: xastore 和xaload指令。具体为:

  • 把一个数组元素加载到操作数栈的指令: baload、caload、 saload、iaload、laload、faload、daload、aaload

  • 将一个操作数栈的值存储到数组元素中的指令: bastore、 castore、sastore、iastore、 lastore、fastore、dastore、 aastore

即:

image-20221021164701313

  • 取数组长度的指令: arraylength 该指令弹出栈顶的数组元素,获取数组的长度,将长度压入栈

说明

  • 指令xaload表 示将数组的元素压栈,比如saload、caload分别表示压入short数组和char数组。指令xaload在执行时,要求操作数中栈顶元素为数组索引 i ,栈顶顺位第2个元素为数组引用a,该指令会弹出栈顶这两个元素,并将a[i]重新压入堆栈

  • xastore则专门针对数组操作,以iastore为例, 它用于给一个int 数组的给定索引赋值 τ 在iastore执行前,操作数栈顶需要以此准备3个元素:值、索引、数组引用,iastore会弹出这3个值,并将值赋给数组中指定索引的位置

代码举例:

    public void setArray() {
        int[] intArray = new int[10];
        intArray[3] = 20;
        System.out.println(intArray[1]);

        // boolean[] arr = new boolean[10];
        // arr[1] = true;
    }

​ 字节码信息:

 0 bipush 10
 2 newarray 10 (int)
 4 astore_1
 5 aload_1
 6 iconst_3
 7 bipush 20
 9 iastore
10 getstatic #8 <java/lang/System.out : Ljava/io/PrintStream;>
13 aload_1
14 iconst_1
15 iaload
16 invokevirtual #14 <java/io/PrintStream.println : (I)V>
19 return

局部变量表和操作数栈对应信息:

image-20221021170203516

数组的 xload、xastore 不同,数组的操作指令主要是操作堆空间的 数组对象,而栈中和局部变量表中保存的是 数组在堆空间的地址。

iastore : 为堆空间中的数组元素赋值,要求栈中有三个元素, 值 + 索引 + 引用

iaload : 要求栈中有俩个元素: 索引 + 引用, 找到堆空间中数组所对应的值并入栈。

类型检查指令

检查类实例或数组类型的指令: instanceof、 checkcast.

  • 指令checkcast用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast指令不会改变操作数栈否则它会抛出ClassCastException异常。

指令instanceof用来判断给定对象是否是某一个类的实例,它会将判断结果压入操作数栈

方法调用与返回指令

方法调用指令

方法调用指令: invokevirtual、 invokeinterface、 invokespecial、invokestatic 、 invokedynamic
以下5条指令用于方法调用:

  • invokevirtual指令 用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态。这也是Java语言中最常见的方法分派方式。

  • invokeinterface指令 用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。

  • invokespecial指令 用于调用-些需要特殊处理的实例方法,包括实例初始化方法 (构造器)、私有 方法和父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发。

  • invokestatic指令 用于调用命名类中的类方法(static方法)。这是静态绑定的。

  • invokedynamic: 调用动态绑定的方法,这个是JDK 1.7后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在java 虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

如果是接口中的 static 方法,使用 invokestatic 指令,除此之外接口中所有的方法都是 invokeinterface 指令

方法返回指令:

方法调用结束前,需要进行返回。方法返回指令是根据返回值的类型区分的。

  • 包括ireturn (当返回值是 boolean、byte、char、short和int 类型时使用)、lreturn、 freturn、dreturn和areturn 另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

image-20221021173803671

操作数管理指令

如同操作一个普通数据结构中的堆栈那样,JVM提供的操作数栈管理指令,可以用于直接操作操作数栈的指令。
这类指令包括如下内容:

  • 将一个或两个元素从栈项弹出,并且直接废弃: pop, pop2;

  • 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup, dup2, dup_ x1,dup2_x1, dup_x2, dup2_x2;

  • 将栈最顶端的两个Slot数值位置交换: swap. Java虛拟机没有提供交换两个64位数据类型(long、double) 数值的指令。

  • 指令nop,是一个非常特殊的指令,它的字节码为0x00。和汇编语言中的nop- -样,它表示什么都不做。这条指令一-般可用于调试、占位等。这些指令属于通用型,对栈的压入或者弹出无需指明数据类型。

说明:

  • 不带_ x的指令是复制栈顶数据并压入栈顶。包括两个指令,dup和dup2。dup的系数代表要复制的Slot个数。

  • dup开头的指令用于复制1个Slot的数据。例如1个int或1个reference类型数据

  • dup2开头的指令用于复制2个Slot的数据。例如1个long,或2个int, 或1个int+1个float类型数据

  • 带_x的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令, dup_x1, dup2_x1,dup__x2, dup2_x2。 对于带_x的复制插入指令,只要将指令的dup和x的系数相加,结果即为需要插入的位置。因此

    • dup_x1插入位置: 1+1=2, 即栈顶2个Slot 下面
    • dup_ x2插入位置: 1+2=3, 即栈顶3个Slot 下面
    • dup2_ x1插入位置: 2+1=3, 即栈顶3个Slot下面
    • dup2 x2插入位置: 2+2=4, 即栈顶4个Slot下面
  • pop:将栈顶的1个Slot数值出栈。例如1个short类型数值

  • pop2:将栈顶的2个Slot数值出栈。例如1个double类型数值,或者2个int类型数值

控制转移指令

程序流程离不开条件控制,为了支持条件跳转,虚拟机提供了大量字节码指令,大体上可以分为

1)比较指令、2)条件跳转指令、3)比较条件跳转指令、4)多条件分支跳转指令、5)无条件跳转指令等。

条件转移指令

条件跳转指令通常和比较指令结合使用。在条件跳转指令执行前,干般可以先用比较指令进行栈项元素的准备,然后进行条件跳转。
条件跳转指令有: ifeq, iflt, ifle, ifne, ifgt, ifge, ifnull, ifnonnull。 这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)。

它们的统一含义为: 弹出栈项元素,测试它是否满足某一一条件,如果满足条件,则跳转到给定位置

具体说明:

image-20221021182821946

条件转移指令通常和比较指令配合使用,由比较指令得出一个 int 值,然后条件转移指令和 0 进行比较。

注意:

  1. 与前面运算规则-一致:

对于boolean、byte、char、 short类型的条件分支比较操作,都是使用int类型的比较指令完成对于long、float、 double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转

  1. 由于各类型的比较最终都会转为int 类型的比较操作,所以Java虚拟机提供的int类型的条件分支指令是最为丰富和强
    大的。

代码举例

    public void compare2() {
        float f1 = 9;
        float f2 = 10;
        System.out.println(f1 < f2);//true
    }

字节码信息:

 0 ldc #2 <9.0>
 2 fstore_1
 3 ldc #3 <10.0>
 5 fstore_2
 6 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
 9 fload_1
10 fload_2
11 fcmpg
12 ifge 19 (+7)
15 iconst_1
16 goto 20 (+4)
19 iconst_0
20 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
23 return

比较得出结果是: 1 或者 0

调用 println 方法会自动转换成 true 或者 false

image-20221021190406325

比较条件转移指令

比较条件跳转指令类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。

这类指令有: if_icmpeq、 if_icmpne、 if_icmplt、 if_icmpgt、 if_icmple、 if_icmpge、if_ acmpeq 和 if_ acmpne

其中指令助记符加上“if_”后,以字符“i”开头的指令针对int型整数操作(也包括short和byte类型),以字符“a”开头的指令表示对象引用的比较。
具体说明:

image-20221021184246831

这些指令都接收两个字节的操作数作为参数,用于计算跳转的位置。同时在执行指令时,栈顶需要准备两个元素进行比较
指令执行完成后,栈顶的这两个元素被清空,且没有任何数据入栈。如果预设条件成立,则执行跳转,否则,继续执行下一一
条语句。

代码举例:

    //2.比较条件跳转指令
    public void ifCompare1(){
        int i = 10;
        int j = 20;
        System.out.println(i > j);
    }

字节码信息:

 0 bipush 10
 2 istore_1
 3 bipush 20
 5 istore_2
 6 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
 9 iload_1
10 iload_2
11 if_icmple 18 (+7)
14 iconst_1
15 goto 19 (+4)
18 iconst_0
19 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
22 return

image-20221021185216418

多条件分支跳转指令

多条件分支指令主要是为了 switch-case 设计的,主要有俩个字节码指令:

image-20221021191629897

从助记符上看,两者都是switch语句的实现,它们的区别:

  • tableswitch要求多 个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高。

  • 指令lookupswitch内 部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低。

无条件跳转指令

目前主要的无条件跳转指令为goto。指令goto接收两个字节的操作数,共同组成一个带符 号的整数,用于指定指令的偏移量指令执行的目的就是跳转到偏移量给定的位置处。

如果指令偏移量太大,超过双字节的带符号整数的范围,则可以使用指令goto_ W, 它和goto有相同的作用,但是它接收4个字节的操作数,可以表示更大的地址范围。

指令jsr、jsr_ W、ret虽然也是无条件跳转的,但主要用于try-finally语句,且已经被虚拟机逐渐废弃,故不在这里介绍这两个指令。

image-20221021192552618

抛出异常指令

(1 ) athrow指令

在Java程序中显示抛出异常的操作 (throw语句) 都是由 athrow指令来实现。除了使用throw语句显示抛出异常情况之外,JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在之前介绍的整数运算时,当除数为零时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常。

(2)注意

正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时,Java 虚拟机会清除操作数栈上的所有内容,而后将异常实例压入调用者操作数栈上。

异常及异常的处理:

过程一:异常对象的生成过程—> throw (手动/自动) —> 指令: athrow
过程二:异常的处理:抓抛模型。 try-catch- finally —> 使用异常表

异常处理与异常表

处理异常:
在Java虚拟机中,处理异常(catch语句) 不是由字节码指令来实现的(早期使用jsr、ret指令),而是采用异常表来完成的。

异常表
如果一个方法定义了一个try-catch或者fry-finally的异常处理,就会创建一一个异常表。 它包含了每个异常处理或者finally块的信息。异常表保存了每个异常处理信息。比如:

  • 起始位置
  • 结束位置
  • 程序计数器记录的代码处理的偏移地址
  • 被捕获的异常类在常量池中的索引

当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。

如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。

不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块, 在return前, 它直接跳到finally块来完成目标

代码举例一

    public void tryCatch(){
        try{
            File file = new File("d:/hello.txt");
            FileInputStream fis = new FileInputStream(file);

            String info = "hello!";
        }catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        catch(RuntimeException e){
            e.printStackTrace();
        }
    }

字节码信息:

image-20221021222001177

黄色部分是出现异常时才会执行的字节码:

image-20221021222031467

代码举例二:

    public static String func() {
        String str = "hello";
        try{
            return str;
        }
        finally{
            str = "atguigu";
        }
    }

输出结果:

hello

字节码信息:

在局部变量表中复制了俩分 hello

image-20221021222739392

同步控制指令

组成

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

方法级的同步:是隐式的

即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的 ACC_ SYNCHRONIZED访问标志得知一个方法是否声明为同步方法;当调用方法时,调用指令将会检查方法的ACC_ SYNCHRONIZED访问标志是否设置。

  • 如果设置了,执行线程将先持有同步锁,然后执行方法。最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁。
  • 在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获得同一个锁。,如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。

image-20221021225322013

同步一段指令集序列

通常是由java中的 synchronized 语句块来表示的。jvm的指 令集有 monitorentermonitorexit两条指令来支持synchronized关键字的语义。

  • 当一个线程进入同步代码块时,它使用monitorenter指令请求进入。如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断持有当前监视器的线程是否为自己,如果是,则进入,否则进行等待,直到对象的监视器计数器为0,才会被允许进入同步块。

  • 当线程退出同步块时,需要使用monitorexit声明退出。在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判对象是否被锁定,当监视器被持有后,对象处于锁定状态。指令monitorenter和monitorexit在执行时,都需要在操作数栈顶压入对象,之后monitorenter和monitorexit的锁定禾释放都是针对这个对象的监视器进行的。

下图展示了监视器如何保护临界区代码不同时被多个线程访问,只有当线程4离开临界区后,线程1、2、3才有可能进入。

image-20221021225515404

对象在堆中存储时,通过对象头中的 锁状态标志、线程持有的锁,判断当前对象是否被锁

image-20221021225601680



各位彭于晏,如有收获点个赞不过分吧…✌✌✌

Alt

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/26758.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

指纹浏览器是什么?可以用来解决跨境电商的什么问题?

如果你是跨境电商中的一员&#xff0c;那我相信你肯定不陌生指纹浏览器吧&#xff01;毕竟指纹浏览器可以说是每个跨境人必备的工具了&#xff0c;更别说它的一系列功能简直是为跨境电商商家量身打造的&#xff01; 龙哥作为跨境老手&#xff0c;对指纹浏览器不要太熟悉&#x…

葡萄糖-顺铂Glucose-cisplatin|葡萄糖-聚乙二醇-顺铂cisplatin-PEG-Glucose

葡萄糖-顺铂Glucose-cisplatin|葡萄糖-聚乙二醇-顺铂cisplatin-PEG-Glucose 中文名称&#xff1a;葡萄糖-顺铂 英文名称&#xff1a;Glucose-cisplatin 别称&#xff1a;生物素修饰葡萄糖&#xff0c;生物素-葡萄糖 PEG接枝修饰葡萄糖 葡萄糖-聚乙二醇-顺铂 cisplatin-PE…

Go:命令行参数解析包 flag 简介

文章目录示例运行小结在 Golang 程序中有很多种方法来处理命令行参数。简单的情况下可以不使用任何库&#xff0c;直接处理 os.Args&#xff1b;其实 Golang 的标准库提供了 flag 包来处理命令行参数&#xff1b;还有第三方提供的处理命令行参数的库&#xff0c;比如 Pflag 等。…

【附源码】计算机毕业设计JAVA研究生推免系统

【附源码】计算机毕业设计JAVA研究生推免系统 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; JAVA myba…

期货开户手机APP有哪些?

一、文华随身行 随身行是文华财经旗下APP软件&#xff0c;提供行情、图表、交易、行情讨论等综合服务。随身行是主流的期货交易软件&#xff0c;已接通全国139家期货公司 不过目前使用文华随身行APP是需要付费的&#xff0c;是直接向软件公司付费&#xff0c;与期货公司无关&…

2022 裁员风潮着实有点大,席卷全球~

近期无论国内外&#xff0c;裁员新闻接连不断&#xff0c;这次裁员风&#xff0c;给人的感觉&#xff0c;像是全球所有公司达成了 “某种协议”&#xff0c;行动上高度一致&#xff0c;开始接二连三的裁员&#xff0c;以往每年各个公司都有裁员&#xff0c;只是比例很小&#x…

【SpringBoot】SpringBoot+SpringSecurity+CAS实现单点登录

文章目录一.CAS的概述1.SSO2.CAS3.概念二.CAS的流程三.CAS服务端部署1.下载地址2.源码打包3.部署运行4. java.io.FileNotFoundException: \etc\cas\thekeystore (系统找不到指定的文件。)四.CAS的定制1.定制数据源2.兼容 HTTP3.定制登录页五.SpringBoot集成CAS1.工程创建2.导入…

2022年程序员“生存报告”出炉,仅23%月薪不足1万,你在什么段位?

转眼2022年仅剩2个月&#xff0c;在充满未知的这一年&#xff0c;程序员群体的职场现状如何&#xff1f; 近日&#xff0c;拉勾招聘数据研究院对程序员群体开展深度调研后&#xff0c;发布了一份《2022程序员群体职场洞察报告》&#xff08;以下简称报告&#xff09;&#xff…

深入理解 MultipartFile 处理文件

在Java中处理文件向来是一种不是很方便的操作&#xff0c;然后随着Spring框架的崛起&#xff0c;使用Spring框架中的MultipartFile来处理文件也是件很方便的事了&#xff0c;今天就为大家带来剖析MultipartFile的神秘面纱。MultipartFile本身并没有很复杂的结构&#xff0c;也正…

DBCO点击试剂特点:DBCO-PEG12-OH,DBCO-PEG12-Tos,DBCO-TCO

DBCO 试剂是一类点击化学标记试剂&#xff0c;含有非常活泼的 DBCO&#xff08;&#xff08;二苯并环辛炔&#xff09;基团&#xff0c;DBCO 试剂可以通过无铜点击化学与叠氮化物标记的分子或生物分子发生反应。DBCO 点击化学可以在水性缓冲液中运行&#xff0c;也可以在有机溶…

pyinstaller打包python脚本为exe可执行文件实例:错误排查小脚本

根据TIOBE全球编程语言排名&#xff0c;python是目前世界排名第一的编程语言。考虑到代码及数据的保密性&#xff0c;以及其他人电脑上可能没有装python环境&#xff0c;因此我们需要将自己编写的python脚本打包成exe格式的可执行文件发送给其他人使用。小编推荐一款名为pyinst…

Spring Boot 2.7.5 整合 Swagger 3 实现可交互的后端接口文档

文章目录前言集成访问代码参考文献前言 问: 什么是 OpenAPI? 答: OpenAPI 规范&#xff08;OAS&#xff09;&#xff0c;是定义一个标准的、与具体编程语言无关的RESTful API的规范。OpenAPI 规范使得人类和计算机都能在“不接触任何程序源代码和文档、不监控网络通信”的情…

智能音箱中采用的数字音频功放

智能改变生活&#xff0c;随高科技的发展智能科技已经融入我们生活当中&#xff0c;智能家居和IOT物联网的发展越来越深入人心&#xff0c;从手机到家电在到家居因为智能化而都在慢慢的改变&#xff1b;智能音响&#xff0c;足不出户&#xff0c;看尽大千世界&#xff1b;一屋一…

《RO 仙境传说》 Game Jam 获奖名单公布!

一睹获胜者的作品吧&#xff5e; 《RO 仙境传说》Game Jam 已经圆满结束&#xff01;许多社区成员都创造了非常棒的体验。 祝贺所有获奖者&#xff0c;并感谢每一位参与并分享了想法的用户。 接下来将公布综合优秀奖、最佳创意奖和最佳设计奖的得主&#xff01;获奖名单是由 Th…

第 21 章 InnoDB Cluster

第21章 InnoDB Cluster 文章目录第21章 InnoDB Cluster本章介绍 MySQL InnoDB Cluster &#xff0c;它整合了 MySQL 多项技术&#xff0c;使您能够部署和管理 MySQL 的完整集成的高可用解决方案。本内容是 InnoDB Cluster 的高级概述&#xff0c;有关完整文档&#xff0c;请参阅…

面试了个阿里P7程序员,他让我见识到什么才是“精通高并发与调优”

蓦然回首自己做开发已经十年了&#xff0c;这十年中我获得了很多&#xff0c;技术能力、培训、出国、大公司的经历&#xff0c;还有很多很好的朋友。但再仔细一想&#xff0c;这十年中我至少浪费了五年时间&#xff0c;这五年可以足够让自己成长为一个优秀的程序员&#xff0c;…

二叉树操作集锦(递归遍历,非递归遍历,求深度,结点个数,完全二叉树等)

二叉树操作集锦&#xff08;递归遍历&#xff0c;非递归遍历&#xff0c;求深度&#xff09; 二叉树操作集锦&#xff08;递归遍历&#xff0c;非递归遍历&#xff0c;求深度&#xff09;一、二叉树操作集锦1.1 二叉树定义1.2 二叉树创建1.3 二叉树遍历1.3.1 二叉树递归遍历1.3…

【JAVA程序设计】基于SSM的电影院在线购票系统-沙箱支付

基于SSM的电影院在线购票系统-沙箱支付项目获取项目简介开发环境项目技术功能结构运行截图项目获取 获取方式&#xff08;点击下载&#xff09;&#xff1a;是云猿实战 项目经过多人测试运行&#xff0c;可以确保100%成功运行。 项目简介 本项目是基于SSM的影院购票系统,本项…

【花雕动手做】有趣好玩的音乐可视化系列项目(32)--P10矩阵LED单元板

偶然心血来潮&#xff0c;想要做一个音乐可视化的系列专题。这个专题的难度有点高&#xff0c;涉及面也比较广泛&#xff0c;相关的FFT和FHT等算法也相当复杂&#xff0c;不过还是打算从最简单的开始&#xff0c;实际动手做做试验&#xff0c;耐心尝试一下各种方案&#xff0c;…

线程,互斥锁,临界区

目录1.线程概念2.windows的线程和linux的线程的区别3虚拟地址到地址空间的转换4.线程优缺点1.优点2.缺点5.进程控制1.创建线程2.线程出现异常了怎么办&#xff1f;进程健壮性问题3.join的第二参数如何理解4.线程终止时6.如果理解pthread_t7.三个概念6.互斥锁1.关于临界区的一点…