Class文件结构和字节码指令集

Class文件结构和字节码指令集

概述

字节码文件的跨平台性

Java 语言:跨平台的语言(write once, run anywhere)
  • 当Java源代码成功编译成字节码之后,如果想在不同的平台上运行,不需要再次编译。
  • 这个优势不是很吸引人了,因为Python、PHP、Perl、Ruby、Lisp等语言都有强大的解释器
  • 跨平台已经几乎快成为一门语言的必选特性
Java 虚拟机:跨语言的平台
  • Java虚拟机不与包含Java在内的任何语言进行绑定,它只和 “Class” 文件这种特定的二进制文件格式关联。无论使用何种语言进行软件开发, 只要能将源文件编译为正确的 Class 文件,那么这种语言就可以在 Java 虚拟机上执行,可以说,统一而强大的 Class 文件结构,就是 Java 虚拟机的基石、桥梁。

image-20220605210251730

JVM规范

所有的 JVM 全部遵守 Java 虚拟机规范,也就是说所有的 JVM 环境都是一样的, 这样一来字节码文件可以在各种 JVM 上进行。

想要让一个 Java 程序正确地运行在 JVM 中,Java 源码就是必须要被编译为符合 JVM 规范的字节码。

前端编译器的主要任务就是负责将符合 Java 语法规范的 Java 代码转换为符合 JVM 规范的字节码文件。

javac 是一种能够将 Java 源码编译为字节码的前端编译器。

javac 编译器在将 Java 源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法分析、语法分析、语义分析以及生成字节码。

image-20220605210516146

Oracle 的 JDK 软件包括两部分内容:

  • 一部分是将 Java 源代码编译成 Java 虚拟机的指令集的编译器
  • 另一部分是用于实现 Java 虚拟机的运行时环境

Java的前端编译器

image-20220605210704208

前端编译器 VS 后端编译器

Java 源代码的编译结果是字节码,那么肯定需要有一种编译器能够将 Java 源码编译为字节码,承担这个重要责任的就是配置在 path 环境变量中的 javac 编译器。javac 是一种能够将 Java 源码编译为字节码的前端编译器

HotSpot VM 并没有强制要求前端编译器只能使用 javac 来编译字节码,其实只要编译结果符合 JVM 规范都可以被 JVM 所识别即可。在 Java 的前端编译器领域,除了 javac 之外,还有一种被大家经常用到的前端编译器,那就是内置在 Eclipse 中的 ECJ (Eclipse Compiler for Java)编译器。和 javac 的全量式编译不同,ECJ 是一种增量式编译器。

  • 在 Eclipse 中,当开发人员编写完代码后,使用"Ctrl + S"快捷键时,ECJ 编译器所采取的编译方案是把未编译部分的源码逐行进行编译,而非每次都全量编译。因此 ECJ 的编译效率会比 javac 更加迅速和高效,当然编译质量和 javac 相比大致还是一样的。
  • ECJ 不仅是 Eclipse 的默认内置前端编译器,在 Tomcat 中同样也是使用 ECJ 编译器来编译 jsp 文件。由于 ECJ 编译器是采用 GPLv2 的开源协议进行源代码公开,所以,大家可以登录 Eclipse 官网下载 ECJ 编译器的源码进行二次开发。
  • 默认情况下,IntelliJ IDEA 使用 javac 编译器(还可以自己设置为 AspectJ 编译器 ajc)

前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给 HotSpot 的 JIT 编译器负责。

透过字节码指令看代码细节

  1. BAT面试题目

① 类文件结构有几个部分?
② 知道字节码吗?字节码都有哪些? Integer x = 5; int y = 5;比较×==y都经过哪些步骤?

  1. 代码举例
public class IntegerTest {

    public static void main(String[] args) {
        Integer x = 5;
        int y = 5;
        System.out.println(x == y); // 

        Integer i1 = 10;
        Integer i2 = 10;
        System.out.println(i1  ==i2);  // true

        Integer i3 = 128;
        Integer i4 = 128;
        System.out.println(i3 == i4); // false
    }
}

Integer的valueOf方法,IntegerCache是一个静态内部类,用于创建-128~127范围内的Integer数组。

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

下面是IntegerCache的静态内部类

image-20220604194054116

image-20220604193655858

image-20220604195245925

实例二:使用Sting来看

public class StringTest {
    public static void main(String[] args) {

        String str = new String("hello") + new String("world");

        String str2 = "helloword";

        System.out.println(str2 == str);  // 输出:false

    }
}

image-20220604200442633

实例三:

class Father {

    int x = 10;



    public Father() {

        this.print();

        x = 20;

    }



    public void print() {

        System.out.println("Father.x = " + x);

    }

}



class Son extends Father {

    int x = 30;



    public Son() {

        this.print();

        x = 40;

    }


    @Override
    public void print() {

        System.out.println("Son.x = " + x);

    }

}

public class _03_SonTest {

    public static void main(String[] args) {

        Father f = new Father();

        System.out.println(f.x);

    }

}

查看Father类的字节码文件

image-20220604202516291

查看son类的字节码文件

image-20220604214221708

Class文件结构

  • 官方文档位置

类文件结构的官方文件位置:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

  • Class 类的本质

任何一个 Class 文件都对应着唯一一个类或接口的定义信息,但反过来说,Class 文件实际上它并不一定以磁盘文件形式存在。Class 文件是一组以8位字节为基础单位的二进制流

  • Class 文件格式

Class 的结构不像 XML 等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

Class 文件格式采用一种类似于 C 语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数

  • 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。
  • (表相当于Java的数组)是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明(因为没有分隔符)。

image-20220605211616136

表用于描述有层次关系的复合结构的数据

image-20220605211652318

整个 Class 文件本质上就是一张表

image-20220605211722803

Class文件结构

image-20220605212650918

也就回答了上面的面试题目

image-20220605212735855

类型名称说明长度数量
u4magic魔数,识别Class文件格式4个字节1
u2minor_version副版本号(小版本)2个字节1
u2major_version主版本号(大版本)2个字节1
u2constant_pool_count常量池计数器2个字节1
cp_infoconstant_pool常量池表n个字节constant_pool_count-1
u2access_flags访问标识2个字节1
u2this_class类索引2个字节1
u2super_class父类索引2个字节1
u2interfaces_count接口计数器2个字节1
u2interfaces接口索引集合2个字节interfaces_count
u2fields_count字段计数器2个字节1
field_infofields字段表n个字节fields_count
u2methods_count方法计数器2个字节1
method_infomethods方法表n个字节methods_count
u2attributes_count属性计数器2个字节1
attribute_infoattributes属性表n个字节attributes_count

查看Demo的字节码解读(了解)

image-20220605205104194

加载与存储指令

加载(load):就是将局部变量压栈到操作数栈中,局部变量可能来自于局部变量表(存储指令),也可能来自于常量池。
存储(store):保存到栈帧的局部变量表
这下面的i、l、d、a标识的类型分别是int(4个字节),float(4个字节),double(8个字节),a是引用类型(4个字节)(其实像byte、short、char、boolean也是用的int一个槽位4个字节来表示的

  • 含有load、push、const就是把局部变量压入到操作数栈当中。
  • 含有store的指令就是把从操作数栈中取出存到局部变量表中。
  1. 作用

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

  1. 常用指令

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

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

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

注意:iload_0和iload 0其实是一个意思,但是iload_0只占用一个字节(只有操作码,操作数包含在操作码中了),二iload 0占用三个字节(操作码一个字节+操作数两个字节);还有像short、byte、char、boolean底层也是int的指令,这样做的目的是减少指令的条数,因为指令条数总共只有2^8,两百多条,后面jdk更新的时候,可能会又出现新的指令

image-20220626173411938

image-20220626173437864

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

操作码后面的#4 #5这些就叫做操作数

image-20220626173640381

字节码执行过程

    //1.局部变量压栈指令Z
    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-20220626173832384

操作数栈(Operand Stacks)
我们知道,Java字节码是Java虚拟机所使用的指令集。因此,它与Java虚拟机基于栈的计算模型是密不可分的。
在解释执行的过程中,每当为Java方法分配栈帧时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,用来存放计算的操作数以及返回的结果。
具体来说便是:执行每条指令之前,Java虚拟机要求该指令的操作数已经被压入操作数栈中。在执行指令时,Java虚拟机会将该指令所需要的操作数弹出,并且将指令的结果重新压入栈中。

示例

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

image-20220626182222133

解释上面的字节码指令,iload_1的意思就是将局部变量表中下标为1的变量的值放入到操作数栈中,iconst_2的意思是将局部变量表中的下标为2的变量的值压入操作数栈中,iadd是弹出操作数栈中的两个变量,然后将它们求和,istore4存储到局部变量表索引为4的位置(m),将k+2的值加入到操作数栈中。ldc #15就是到常量池中取出索引为15的变量的值(atguigu),放入到操作数栈中。astore 7就是弹出操作数栈中的栈顶的元素存储到局部变量下标为7的位置,ldc #16的意思是从局部变量表中取出下标为16的变量的值(10.0)放入到操作数栈中,fstore 8 就是从操作数栈中弹出栈顶的元素然后将其存到局部变量表下标为8的位置ldc2_w #17的意思是将常量池中下标为17的变量(10.0)的值放入到操作数栈中,dstore_2的意思是从栈顶弹出10.0然后加入到局部变量表中索引为2的位置,return然后终止方法的执行。

 0 iload_1
 1 iconst_2
 2 iadd
 3 istore 4
 5 ldc2_w #13 <12>
 8 lstore 5
10 ldc #15 <atguigu>
12 astore 7
14 ldc #16 <10.0>
16 fstore 8
18 ldc2_w #17 <10.0>
21 dstore_2
22 return

i++和++i字节码详细分析

不涉及其他运算的情况下

如果不涉及到其他的运算的情况下,i++和++i的字节码是一样的。

3 iinc 1 by 1字节码指令的意思是直接将局部变量表中的索引为1的变量的值+1

image-20220612142237862

    //关于(前)++和(后)++
    public void method6(){
        int i = 10;
//        i++;
        ++i;
    }

后加加的代码以及字节码

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

image-20220612141622807

前加加的代码和字节码

image-20220612141654053

涉及其他运算

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

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

image-20220612142928198

解释下面的字节码指令
0 bipush 10:将10压入到操作数栈中,istore_1:将操作数栈中的栈顶的元素弹出,放入到局部变量表中的索引为1的位置(为0的位置的值是this),iload_1:将局部变量表中的索引为1的变量的值加载到操作数栈中,iinc 1 by 1:将局部变量表中索引为1的变量的值+1(也就是变量i),istore_2:将操作数栈中的栈顶元素(10)弹出,放在局部变量表中索引为2的位置(也就是变量a),bipush 20:将20压入操作数栈中,istore_3:从操作数栈顶弹出元素(20)放到局部变量表中索引为3的位置(也就是j所对应的值),iinc 3 by 1 :将局部变量表中的索引为3的变量的值(j的值)加1,就变成21了,iload_3:加载局部变量表中索引为3的变量的值(21)放到操作数栈顶。istore 4:从栈顶弹出21存到局部变量表索引为4的位置,return:然后结束方法。

局部变量表和操作数栈的简图

image-20220612144545525
 0 bipush 10
 2 istore_1
 3 iload_1
 4 iinc 1 by 1
 7 istore_2
 8 bipush 20
10 istore_3
11 iinc 3 by 1
14 iload_3
15 istore 4
17 return

i = i++

代码

    public void method8(){
        int i = 10;
        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>
11 iload_1
12 invokevirtual #5 <java/io/PrintStream.println>
15 return

image-20220612145500590

算数指令

所有指令

  1. 加法指令:iadd、ladd、fadd、dadd
  2. 减法指令:isub、lsub、fsub、dsub
  3. 除法指令:idiv、ldiv、fdiv、ddiv
  4. 求余指令:irem、lrem、frem、drem // remainder:余数的意思
  5. 自增指令:iinc
  6. 位运算指令,又可分为:
    1. 位移指令:ishl、 ishr、 iushr、 lshl、lshr、lushr
    2. 按位或指令:ior、lor
    3. 按位与指令:iand、land
    4. 按位异或指令:ixor、lxor
  7. 比较指令:dcmpg、dcmpl、 fcmpg、fcmpl、 1cmp

image-20220626182846909

i = i + 10和i+=10的区别

i+=10的情况下

源码

    public void method3(int j) {
        int i = 100;
        i += 10;
    }

字节码

0 bipush 100
2 istore_2
3 iinc 2 by 10 // 这里是直接将局部变量表中索引为2的变量的值+10
6 return

i = i + 10的情况

    public void method3(int j) {
        int i = 100;
        i = i + 10;
    }
0 bipush 100
2 istore_2
3 iload_2 // 加载局部变量表中索引为2的变量的地址值,放到操作数栈当中
4 bipush 10 // 往操作数栈当中压入10
6 iadd // 弹出操作数栈中的两个变量,相加,压入操作数栈
7 istore_2 // 存到局部变量表索引为2的位置 (值为110)
8 return

其实主要区别就是:

i+=10;是直接操作局部变量表

i=i + 10;是先将i的值加载到操作数栈当中,然后再对i进行操作

类型转换指令

宽化类型转化

宽化类型转化:从小范围向大范围转换。(也叫做自动类型提升,自动的就会有个转换了)
简化为:int --> long --> float --> double(int可以转换成long、float、double,long转换成float、double。float转换成double)

image-20220612152843577

精度损失问题

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

int、long类型数值转换到float,或者long类型数值转换到double时,将可能发生精度丢失——可能丢失掉几个最低有效位上的值,转换后的浮点数值是根据IEEE754最接近舍入模式所得到的正确整数值。

尽管宽化类型转换实际上是可能发生精度丢失的,但是这种转换永远不会导致Java虚拟机抛出运行时异常。

    //举例:精度损失的问题
    @Test
    public void upCast2(){

        int i = 123123123;
        float f = i;
        // 输出结果1.2312312E8,这里就相当于发生了精度丢失变成了123123120
        System.out.println(f);//123123120

        long l = 123123123123L;
        l = 123123123123123123L;
        double d = l;
        // 1.2312312312312312E17这里也发生了精度丢失,变成了123123123123123120
        System.out.println(d);//123123123123123120

    }

byte、short类型在底层都是用int来进行存的

//针对于byte、short等转换为容量大的类型时,将此类型看做int类型处理。
public void upCast3(byte b){
    int i = b;
    long l = b;
    double d = b;
}

image-20220612163142046

注意:从float、double、long等类型往byte、short、char类型转换的时候,需要先把前面几种类型转换成int类型,然后在从int类型转换到后面这几种类型,所以int类型相等于一种过渡类型

image-20220612165149134

image-20220612165627145

窄化类型转换的精度丢失,将int i = 128;强转成 byte b;会将前面的24位给丢失,也就是变成了10000000,也就是-128了

    //窄化类型转换的精度损失
    @Test
    public void downCast4(){
        // 128是32位的前24位都是0,后8位数是10000000
        // 将int转换成byte之后就会砍掉前面的24位,然后后面就变成-128了
        int i = 128;
        byte b = (byte)i;
        System.out.println(b);
    }

补充说明

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

    • 如果浮点值是NaN,那抓换的结果就是int或者long类型的0

    • 如果浮点值不是无穷大的话,浮点值使用IEEE 754的向零舍入的模式取整,获得整数值v

    • 如果v在目标类型T(int或long)的表示范围之内,那转化的结果就是v。否则,根据v的符号,转换为T所能表示的最大或者最小整数

  2. 当将一个double类型转换成float类型时,将遵循以下转换规则:

    • 如果转换结果的绝对值太小而无法使用float来表示,将返回float类型的正负0
    • 如果转换结果的绝对值太大而无法使用float来表示,将返回float类型的正负无穷大
    • 对于double类型的NaN值将按照规定转换为float类型的NaN值

对象的创建与访问指令

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

创建指令

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

  1. 创建类实例的指令

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

    • 创建数组的指令:newarray、anewarry、multianewarry
      • newarry:创建基本类型数组
      • anewarry:创建引用类型数组 (如果是引用数据类型的数组,就在数组前面加a,表示是引用数据类型的数组)
      • multianewarry:创建多维数组 (这里是muti+a+newarray可以看作是数组里面有数组)

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

  1. 创建类的实例

image-20220615152121548

  1. 创建数组的对象

image-20220615160519938

字段访问指令

可以按照是否含有static来划分
对象创建后,就可以通过对象访问指令获取对象实例或数组实例中的字段或者数组元素。

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

    • getstatic表示的是将类字段压入到操作数栈当中(获取类或接口字段的值并将其推送到操作数堆栈上。)
    • putstatic表示的是将操作数栈弹出
  • 访问类实例字段(非static字段,或者称为实例变量)的指令: getfield、putfield

    • getfield:将实例变量压入到操作数栈当中
    • putfield:将实例变量从操作数栈中弹出

image-20220615162404045

    public void setOrderId(){
        Order order = new Order();
        order.id = 1001;
        System.out.println(order.id);

        Order.name = "ORDER";
        System.out.println(Order.name);
    }

image-20220615164713714

数组操作指令

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

  • 把一个数组元素加载到操作数栈的指令: baloadcaloadsaloadialoadlaloadfaloaddaloadaaload
  • 将一个操作数栈的值存储到数组元素中的指令: bastorecastoresastoreiastorelastorefastoredastoreaastore

可以从下面看到bzyteboolean的加载和存储指令都是使用baloadbastore(其实boolean也是用数字来表示的,非0表示正数,0表示负数)

image-20220615184754425

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

说明:

  • 指令xaload表示将数组的元素压栈,比如saload、caload分别表示压入short数组和char数组。指令xaload在执行时,要求操作数中栈顶元素为数组索引i栈顶顺位第2个元素为数组引用a,该指令会弹出栈顶这两个元素,并将a[i]重新压入堆栈
  • xastore则专门针对数组操作,以iastore为例,它用于给一个int数组的给定索引赋值。在iastore执行前,操作数栈顶需要以此准备3个元素:值、索引、数组引用iastore会弹出这3个值,并将值赋给数组中指定索引的位置

下面那张图中的标红的部分就使用到了这两条指令

    //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;
    }

image-20220615184231401

将boolean数组注释打开之后

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

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

image-20220615185619982

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

image-20220615213357992

类型检查

检查类实例或数组类型的指令: instanceofcheckcast
指令checkcast用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast指令不会改变操作数栈,否则它会抛出ClassCastException异常
指令instanceof用来判断给定对象是否是某一个类的实例,它会将判断结果压入操作数栈。

    //4.类型检查指令
    public String checkCast(Object obj) {
        if (obj instanceof String) {
            return (String) obj;
        } else {
            return null;
        }
    }

image-20220615220006514

方法调用与返回

方法调用指令

方法调用指令:invokevirtualinvokeinterfaceinvokespecialinvokestaticinvokedynamic

以下5条指令用于方法调用:

  • invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),支持多态。这也是Java语言中最常见的方法分派方式。(动态分派或者动态绑定)
  • invokeinterface指令用于调用接口方法,它会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。
  • invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法〈构造器)、私有方法和父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发。(这几种方法不存在方法的重写)
  • invokestatic指令用于调用命名类中的类方法(static方法)。这是静态绑定的。注意:静态方法是不能重写的
  • invokedynamic:调用动态绑定的方法,这个是JDK 1.7后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。前面4条调用指令的分派逻辑都固化在java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

invokevirtual 体现为晚期绑定

invokeinterface 也体现为晚期绑定

invokespecial 体现为早期绑定

注意:

  1. invokedynamic老师不讲,估计是很少遇到吧
  2. invokeinterface是对接口而言的,用属于接口类型的对象调用方法的时候就是这个(但是如果调用的是接口中的静态方法字节码指令是invokestatic
  3. invokespecial只有构造器、私有方法、super.方法名()调用父类方法这几种情况,其中调用父类方法这种情况可能出现其直接父类没有该方法,那就可以调用其父类继承的父类中的该方法,最终找到一个方法调用就是了
  4. invokestatic是调用static静态方法,无论是使用对象.静态方法名()还是类名.静态方法名()都是invokestatic,也不难理解
  5. invokevirtual是调用类中的非静态普通方法,而这种实例方法可能调用的是子类重写的非静态普通方法,比如A a = new B();a.hello(),其中B类继承A类,并且B类重写了A类中的hello()方法,这种情况下就是invokevirtual了,但是有可能该类没有子类,调用的就是本类中的非静态普通方法,这种情况也是invokevirtual了(其实也就是除了上面的几种都属于invokevirtual

这下面有几个例子用来解释说明invokespecial指令(其实这些也就是在编译器就确定好了方法的调用,而不是在运行期间再确定的)

/**
 * @author shkstart
 * @create 2020-09-08 9:35
 *
 * 指令5:方法调用与返回指令
 */
public class _5MethodInvokeReturnTest {

    //方法调用指令:invokespecial:静态分派
    public void invoke1(){
        //情况1:类实例构造器方法:<init>()
        Date date = new Date();

        Thread t1 = new Thread();
        //情况2:父类的方法
        super.toString();

        //情况3:私有方法
        methodPrivate();
    }

    private void methodPrivate(){

    }
    //方法调用指令:invokestatic:静态分派
    public void invoke2(){
        methodStatic();
    }

    // 如果使用private + static来进行修饰的化,
    // 底层的字节码指令还是invokestatic:编译的时候就确定好了的
    // 我们叫做静态分派
    private static void methodStatic(){

    }

    //方法调用指令:invokeinterface
    public void invoke3(){
        Thread t1 = new Thread();
        ((Runnable)t1).run();

        Comparable<Integer> com = null;
        com.compareTo(123);
    }

    //方法调用指令:invokeVirtual:动态分派
    public void invoke4(){
        System.out.println("hello");

        Thread t1 = null;
        t1.run();
    }
    //方法的返回指令
    public int returnInt(){
        int i = 500;
        return i;
    }

    public double returnDouble(){
        return 0.0;
    }

    public String returnString(){
        return "hello,world";
    }

    public int[] returnArr(){
        return null;
    }
    public float returnFloat(){
        int i = 10;
        return i;
    }

    public byte returnByte(){
        return 0;
    }
    public void methodReturn(){
        int i = returnByte();
    }
}

方法调用指令的补充说明,可以看到下面的aa.method2();的字节码指令是invokeinterface,执行method2的字节码指令是invokestatic(因为在接口AA中method2是用static修饰的,所以是invokestatic)

/**
 * @author shkstart
 * @create 2020-09-10 17:26
 * 补充:方法调用指令的补充说明
 */
public class _6InterfaceMethodTest {
    public static void main(String[] args) {
        AA aa = new BB();

        //  9 invokeinterface #4 <com/atguigu/java/AA.method2>
        aa.method2();

        // 14 invokestatic #5 <com/atguigu/java/AA.method1>
        AA.method1();
    }
}


interface AA {
    public static void method1() {
    }

    public default void method2() {
    }
}

class BB implements AA {

}

虚方法与非虚方法(复习,之前在上篇类的加载与垃圾回收篇的第四章的时候讲过,这里只是复制之前的内容)

虚方法与非虚方法的区别

  1. 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法
  2. 静态方法、私有方法、final方法、实例构造器(重载)、父类方法都是非虚方法(子类通过super.方法的方式调用父类的方法,因为都是非常确定的)。(前三个不能重写)
  3. 其他方法称为虚方法

不管子类父类他们的那个方法能被重写,那这方法就是虚方法。

子类对象的多态的使用前提:

  1. 类的继承关系,或者接口的实现
  2. 方法的重写

虚拟机中调用方法的指令

  • 普通指令:

    1. invokestatic:调用静态方法,解析阶段确定唯一方法版本 (非虚方法)
    2. invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本 (非虚方法)
    3. invokevirtual:调用所有虚方法
    4. invokeinterface:调用接口方法
  • 动态调用指令
    5. invokedynamic:动态解析出需要调用的方法,然后执行

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预。而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法其余的(final修饰的除外)称为虚方法。

方法返回指令

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

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

image-20220617091227781

举例:
通过ireturn指令,将当前函数操作数栈的顶层元素弹出,并将这个元素压入调用者函数的操作数栈中(因为调用者非常关心函数的返回值),所有在当前函数操作数栈中的其他元素都会被丢弃。

如果当前返回的是synchronized方法,那么还会执行一个隐含的monitorexit指令,退出临界区。

最后,会丢弃当前方法的整个帧,恢复调用者的帧,并将控制权转交给调用者。

image-20220617091438903

下面是代码举例:

 //方法的返回指令
    public int returnInt() {
        int i = 500;
        return i;
    }

    public double returnDouble() {
        return 0.0;
    }

    public String returnString() {
        return "hello,world";
    }

    public int[] returnArr() {
        return null;
    }

    public float returnFloat() {
        int i = 10;
        return i;
    }

    public byte returnByte() {
        return 0;
    }

    public boolean returnBoolean(){
        return false;
    }
    public char returnChar(){
        return 'a';
    }

    public void methodReturn() {
        int i = returnByte();
    }

image-20220617101217381

操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,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类型数值

操作数栈的管理指令,下面是几个方法的简单举例

代码:

    public void print(){
        Object obj = new Object();
//        String info = obj.toString();
        obj.toString();
    }

image-20220617151844434

示例二

代码:

    //类似的
    public void foo(){
        bar();
    }
    public long bar(){
        return 0;
    }

image-20220617152553702

示例三

代码:

public long nextIndex() {
        return index++;
    }

private long index = 0;

图示分析上面代码的操作数栈的调用过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cBad03KY-1656240361662)(C:/Users/losser/AppData/Roaming/Typora/typora-user-images/image-20220617160354229.png)]

控制转移指令

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

  1. 比较指令(其实这里的比较指令归结到了算数指令里面去了)
  2. 条件跳转指令
  3. 比较条件跳转指令
  4. 多条件分支跳转指令
  5. 无条件跳转指令等

比较指令

比较指令的说明

  • 比较指令是比较栈顶的两个元素的大小,并将比较的结果入栈。
  • 比较指令有:dcmpg、 dcmpl、fcmpg、 fcmp1、lcmp。
    • 与前面讲解的指令类似,首字符d表示double类型,f表示float,l表示long。
  • 对于double和float类型的数字,由于NaN的存在,各有两个版本的比较指令。以float为例,有fcmpg和fcmpl两个指令,它们的区别在于在数字比较时,若遇到NaN值,处理结果不同。(这里注意要去复习之前的算数指令那一块)(简单来说一下这个fcmpg和cmpl这两个指令的英文,更加容易记住f值得是float,同理d表示的double,cmp表示的就是compare,那个g表示的就是greater,l就表示的是less,所以也就不难理解g放的是1而l放的是-1)
  • 指令dcmpl和dcmpg也是类似的,根据其命名可以推测其含义,在此不再赘述。指令lcmp针对long型整数,由于long型整数没有NaN值,故无需准备两套指令。

举例:

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

注意:

数值类型的变量才可以比较大小!

boolean、引用数据类型不能比较大小

注意:NaN(Not a Number)表示不是一个数字,比如0.0/0.0得到的可能是1.0(两个数相等),也可能是0.0(0.0是分子),也可能是无穷大(0.0是分母),所以老师给出的解释是NaN代表无法确定是什么数字,只有double和float类型中可能出现NaN的情况,而long类型不会出现NaN,所以只有lcmp,而没有lcml

条件跳转指令

条件跳转指令的要点说明

条件**跳转指令通常和比较指令结合使用**。在条件跳转指令执行前,一般可以先用比较指令进行栈顶元素的准备,然后进行条件跳转

条件跳转指令有:ifeq,iflt,ifle,ifne,ifgt,ifge,,ifnull, ifnonnull。这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset)。

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

说明:这里的eq表示的就是equals,l表示的是less,le表示的是less equals,g表示的是greater

image-20220621070140612

说明:这里的eq表示的就是equals,l表示的是less,le表示的是less equals,g表示的是greater

注意:

  1. 与前面的运算符一致:
    • 对于boolean、byte、char、short类型的条件分支比较操作,都是使用int类型的比较指令完成
    • 对于long、float、double类型的条件分支比较操作,则会**先执行相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈**中,随后再执行int类型的条件分支比较操作来完成整个分支跳转
条件跳转指令字节码解析实例
//1.条件跳转指令
public void compare1(){
    int a = 0;
    if(a != 0){
        a = 10;
    }else{
        a = 20;
    }
}

image-20220621083553284

示例一
    //结合比较指令
    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>
 9 fload_1
10 fload_2 
11 fcmpg
12 ifge 19 (+7)  // 这里是判断栈顶的元素是否大于等于0,是的话就跳转到19
15 iconst_1
16 goto 20 (+4)
19 iconst_0
20 invokevirtual #5 <java/io/PrintStream.println>
23 return

image-20220623065319428

就是这个方法

image-20220623065802770

示例二
    public void compare3() {
        int i1 = 10;
        long l1 = 20;
        System.out.println(i1 > l1);
    }

字节码指令

 0 bipush 10  // 往操作数栈中压入10
 2 istore_1   // 存到局部变量表中的索引为1的位置
 3 ldc2_w #6 <20>  // 从常量池中加载20,放到操作数栈
 6 lstore_2 // 存到局部变量表索引为1的位置,同时出栈
 7 getstatic #4 <java/lang/System.out> // 获取System.out,放入操作数栈
10 iload_1 // 加载局部变量表中索引为1的变量,也就是10
11 i2l // 将int 10转成long 10,然后再压入操作数栈
12 lload_2 // 加载局部变量表中索引为2的变量
13 lcmp // 然后将两个long类型的变量进行比较,也就是弹出两个元素,由于下面的10是v1,20是v2,所以这里是得到-1,然后再压入操作数栈
14 ifle 21 (+7) // 如果小于等于0,就跳转到21
17 iconst_1 // 从常量池中加载1
18 goto 22 (+4)
21 iconst_0 // 从常量池中加载0,这里boolean的0表示false,非0表示true
22 invokevirtual #5 <java/io/PrintStream.println> // 然后再打印
25 return

注意:

指令fcmpg和fcmpl都从栈中弹出两个操作数,并将它们作比较,设栈顶的元素为v2,栈顶的顺位第2位元素为v1,若v1=v2,则压入0;若v1>v2则压入1;若v1<v2则压入-1.

比较条件转移指令

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

这类指令有:if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。其中指令助记符加上“if_”后,以字符“i”开头的指令针对int型整数操作(也包括short和byte类型),以字符**“a”开头的指令表示对象引用的比较**。

具体说明:

image-20220623072800529

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

多条分支跳转

image-20220623073729529

image-20220623073740744

image-20220623075823419

image-20220623073746066

多条件分支跳转指令是专为switch-case语句设计的,主要有tableswitch和lookupswitch。

image-20220623074045803

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

  • tableswitch要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量位置,因此效率比较高。
  • 指令lookupswitch内部存放着各个离散的case-offset对每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低。

指令tableswitch的示意图如下图所示。由于tableswitch的case值是连续的,因此只需要记录最低值和最高值,以及每一项对应的offset偏移量,根据给定的index值通过简单的计算即可直接定位到offset。

image-20220623081840505

指令lookupswitch处理的是离散的case值,但是出于效率考虑,将case-offset对按照case值大小排序,给定index时,需要查找与index相等的case,获得其offset,如果找不到则跳转到default。指令lookupswitch 如下图所示。

可以看到这下面已经帮我排好序了

image-20220623082008851

这下面是字符串多条件分支的跳转指令
//jdk7新特性:引入String类型
public void swtich3(String season){
    switch(season){
        case "SPRING":break;
        case "SUMMER":break;
        case "AUTUMN":break;
        case "WINTER":break;
    }
}

它在底层其实会使用equals和hashCode进行判断,下边截取了部分代码

  5 invokevirtual #11 <java/lang/String.hashCode>
  8 lookupswitch 4
	-1842350579:  52 (+44)
	-1837878353:  66 (+58)
	-1734407483:  94 (+86)
	1941980694:  80 (+72)
	default:  105 (+97)
         52 aload_2
 53 ldc #12 <SPRING>
 55 invokevirtual #13 <java/lang/String.equals>
这里是使用tableswith多条件分支值是连续的
    //3.多条件分支跳转
    public void swtich1(int select){
        int num;
        switch(select){
            case 1:
                num = 10;
                break;
            case 2:
                num = 20;
                break;
            case 3:
                num = 30;
                break;
            default:
                num = 40;
        }
    }

注意这里下面操作完了之后都回跳转到49(goto 49),因为我们上面的代码上面加上了break

 0 iload_1
 1 tableswitch 1 to 3	1:  28 (+27)
	2:  34 (+33)
	3:  40 (+39)
	default:  46 (+45)
28 bipush 10
30 istore_2
31 goto 49 (+18) 
34 bipush 20
36 istore_2
37 goto 49 (+12)
40 bipush 30
42 istore_2
43 goto 49 (+6)
46 bipush 40
48 istore_2
49 return

这个是不加break的代码,会造成穿透,也就说,它会执行下面的代码知道遇到了break

    public void swtich1(int select){
        int num;
        switch(select){
            case 1:
                num = 10;
                break;
            case 2:
                num = 20;
//                break;   这里没有加break
            case 3:
                num = 30;
                break;
            default:
                num = 40;
        }
    }

字节码指令

 0 iload_1
 1 tableswitch 1 to 3	1:  28 (+27)
	2:  34 (+33)
	3:  37 (+36)
	default:  43 (+42)
28 bipush 10
30 istore_2
31 goto 46 (+15)
34 bipush 20
36 istore_2
37 bipush 30
39 istore_2
40 goto 46 (+6)
43 bipush 40
45 istore_2
46 return

这里从第9行一直运行到了第13行,这是由于穿透造成的。

无条件跳转

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

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

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

image-20220623211236474

循环结构下的无条件跳转

实例一

Java源代码

    //4.无条件跳转指令
    public void whileInt() {
        int i = 0;
        while (i < 100) {
            String s = "atguigu.com";
            i++;
        }
    }

编译之后生成的字节码,简单分析一下

 0 iconst_0 // 从常量池中加载0,放入操作数栈
 1 istore_1 // 从操作数栈中取出1,存到局部变量表
 2 iload_1  // 从操作数中加载1
 3 bipush 100 // 往操作数栈中压入100
 5 if_icmpge 17 (+12) // 条件判断语句,判断v1是否大于等于v2,是的话就直接跳转到17,终止方法
 8 ldc #17 <atguigu.com> // 加载atguigu.com
10 astore_2 // 存到局部变量表的索引为2的位置
11 iinc 1 by 1 // 然后将局部变量表中的1直接加
14 goto 2 (-12) // 直接返回2,然后又跳转到字节码为2的语句
17 return

可以看到上面的while循环只不过是条件判断指令和无条件跳转指令

这是每一次的步骤

image-20220623214738049

Java源代码,自己尝试解析一下double类型的循环

示例二

    public void whileDouble() {
        double d = 0.0;
        while(d < 100.1) {
            String s = "atguigu.com";
            d++;
        }
    }

字节码指令如下:

 0 iconst_0 // 从常量池中加载0到操作数栈中
 1 istore_1 // 取出操作数栈中变量的值,放入到局部变量表中的索引为1的位置
 2 iload_1 // 从局部变量表中加载1,放到操作数栈中
 3 bipush 100 // 往操作数栈中压入100
 5 if_icmpge 17 (+12) // 弹出操作数中的两个元素进行比较,判断是否大于等于(其实也就是判断v1 >= v2 false),所以直接返回-1,压入到操作数栈当中,如果是大于等于的话,那直接跳转到17,结束方法了
 8 ldc #17 <atguigu.com> // 从常量池中加载atguigu.com
10 astore_2 // 然后将操作数栈弹出元素,存入到局部变量表索引为2的位置
11 iinc 1 by 1 // 将局部变量表中索引为1的位置的元素的值+1
14 goto 2 (-12) // 跳转到2
17 return

示例四

再一个代码实例,这里我就不再赘述了,只贴一下代码和字节码,其他的应该都能看懂。

    public void printFor() {
        short i;
        for (i = 0; i < 100; i++) {
            String s = "atguigu.com";
        }

    }
 0 iconst_0
 1 istore_1
 2 iload_1
 3 bipush 100
 5 if_icmpge 19 (+14)
 8 ldc #17 <atguigu.com>
10 astore_2
11 iload_1
12 iconst_1
13 iadd
14 i2s
15 istore_1
16 goto 2 (-14)
19 return

实例五,其实这下面两个都是差不多的,非要说区别,其实就是它俩int i的作用域不同

    //思考:如下两个方法的操作有何不同?
    public void whileTest(){
        int i = 1;
        while(i <= 100){

            i++;
        }
        //可以继续使用i
    }
    public void forTest(){
        for(int i = 1;i <= 100;i++){

        }
        //不可以继续使用i
    }

image-20220623225142904

异常处理指令

抛出异常指令
  1. 抛出异常指令

(1)athrow指令

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

(2)注意

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

异常及异常的处理

过程一:异常对象的生成过程—>throw (手动 / 自动)—> 指令:athrow

过程二:异常的处理:抓抛模型。try-catch-finally —> 使用异常表(之前看字节码的时候看过)

注意:系统自动抛出的异常如ArithmeticException,系统会自动处理,不用手动throw或者try-catch-finally,在异常表中也不会生成

示例1

image-20220624211215532

Java代码如下

    public void throwZero(int i) {
        if (i == 0) {
            throw new RuntimeException("参数值为0");
        }
    }

字节码指令如下

 0 iload_1
 1 ifne 14 (+13)
 4 new #2 <java/lang/RuntimeException>
 7 dup
 8 ldc #3 <参数值为0>
10 invokespecial #4 <java/lang/RuntimeException.<init>>
13 athrow
14 return
示例2

Java代码

    public void throwOne(int i) throws RuntimeException, IOException {
        if (i == 1) {
            throw new RuntimeException("参数值为1");
        }
    }

字节码指令,这里稍微有一点不同,这里的字节码指令是if_icmpne(是比较指令),而上面是ifne(条件跳转指令)

 0 iload_1
 1 iconst_1
 2 if_icmpne 15 (+13)
 5 new #2 <java/lang/RuntimeException>
 8 dup
 9 ldc #5 <参数值为1>
11 invokespecial #4 <java/lang/RuntimeException.<init>>
14 athrow
15 return

image-20220624213335640

其实这个code属性刻画的是方法体里面的内容,throws是在方法的申明处

示例3(特例)

java代码

public void throwArithmetic() {
    int i = 10;
    int j = i / 0;
    System.out.println(j);
}

字节码指令,因为10/0会出现异常的,但是这里不会出现athrow的,系统已经定义好的异常,它会自动抛出异常。(如:ArithmeticException)

 0 bipush 10
 2 istore_1
 3 iload_1
 4 iconst_0
 5 idiv
 6 istore_2
 7 getstatic #6 <java/lang/System.out>
10 iload_2
11 invokevirtual #7 <java/io/PrintStream.println>
14 return=

异常处理与异常表

处理异常

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

异常表

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

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

当一个异常被抛出时,JVN会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(再调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。

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

Java源代码如下

    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();
        }
    }

字节码如下。

 0 new #8 <java/io/File>
 3 dup
 4 ldc #9 <d:/hello.txt>
 6 invokespecial #10 <java/io/File.<init>>
 9 astore_1
10 new #11 <java/io/FileInputStream>
13 dup
14 aload_1
15 invokespecial #12 <java/io/FileInputStream.<init>>
18 astore_2
19 ldc #13 <hello!>
21 astore_3
22 goto 38 (+16)
25 astore_1
26 aload_1
27 invokevirtual #15 <java/io/FileNotFoundException.printStackTrace>
30 goto 38 (+8)
33 astore_1
34 aload_1
35 invokevirtual #16 <java/lang/RuntimeException.printStackTrace>
38 return

image-20220626111718125

try-catch字节码指令结合局部变量表和操作数栈分析如下。

image-20220625091834452

示例

下面的try-catch-finally执行的结果是hello

//思考:如下方法返回结果为多少?
public static String func() {
    String str = "hello";
    try {
        return str;
    } finally {
        str = "atguigu";
    }
}

public static void main(String[] args) {

    System.out.println(func());//hello
}

字节码指令如下

 0 ldc #17 <hello>
 2 astore_0
 3 aload_0
 4 astore_1
 5 ldc #18 <atguigu>
 7 astore_0
 8 aload_1
 9 areturn
10 astore_2
11 ldc #18 <atguigu>
13 astore_0
14 aload_2
15 athrow

image-20220625094027189

同步控制指令

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

方法级的同步

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

  • 如果设置了,执行线程将先持有同步锁,然后执行方法。最后在方法完成(无论是正常完成还是非正常完成)时释放同步锁。(如果不释放的话,就会出现死锁)
  • 在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获得同一个锁。
    • 回顾一下之前学的知识,如果synchronized修饰的方法,锁的是当前调用的对象,也就是this对象
    • synchronized修饰的是代码块的话,锁的对象就是小括号里面的对象
  • 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。

这下面那个是加上synchronized的方法的。

image-20220625102709744

下面的是没有加synchronized关键字的方法,其实可以看到他们的字节码是没有区别的,但是方法的访问标志信息是不一样的。因为它是隐式级的同步机制。

image-20220625102919990

其Java代码和字节码如下

  private int i = 0;
    public  void add(){
        i++;
    }
 0 aload_0
 1 dup
 2 getfield #2 <com/atguigu/java1/_3SynchronizedTest.i>
 5 iconst_1
 6 iadd
 7 putfield #2 <com/atguigu/java1/_3SynchronizedTest.i>
10 return
monitorenter指令的简单说明

官网地址

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

  • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
  • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
  • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

翻译如下:

每个对象都与一个监视器相关联。当且仅当监视器具有所有者时,它才会被锁定。执行 monitorenter 的线程尝试获取与 objectref 关联的监视器的所有权,如下所示:

  • 如果与 objectref 关联的监视器的条目计数为零(其实也就是监视计数器),则线程进入监视器并将其条目计数设置为 1。然后线程是监视器的所有者。
  • 如果线程已经拥有与 objectref 关联的监视器,它会重新进入监视器,并增加其条目计数。(就是将条目数+1)
  • 如果另一个线程已经拥有与 objectref 关联的监视器,则该线程会阻塞,直到监视器的条目计数为零,然后再次尝试获得所有权。

其实这部分内容和JUC是重合的,后续学JUC的时候,再回顾来看。或许有不一样的收获。

monitorexit指令说明

摘自官网

The objectref must be of type reference.

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.

The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

翻译如下:

objectref 必须是引用类型。

执行 monitorexit 的线程必须是与 objectref 引用的实例关联的监视器的所有者。

该线程减少与 objectref 关联的监视器的条目计数。如果结果条目计数的值为零,则线程退出监视器并且不再是它的所有者。允许其他阻塞进入监视器的线程尝试这样做。

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

回忆上篇的第7章:对象的实例化内存布局与访问定位,对象头里面就包含线程锁持有锁的信息

对象头中含有的信息:

image-20220626114439433

同步一段指令集序列:通常是由java中的synchronized语句块来表示的。jvm的指令集有 monitorenter 和**monitorexit**两条指令来支持synchronized关键字的语义。

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

当线程退出同步块时,需要使用**monitorexit**声明退出。在Java虚拟机中,任何对象都有一个监视器与之相关联,用来判断对象是否被锁定,当监视器被持有后,对象处于锁定状态

指令**monitorentermonitorexit**在执行时,都需要在操作数栈顶压入对象,之后monitorenter和monitorexit的锁定和释放都是针对这个对象的监视器进行的。

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

其实吧,这种字节码文件的分析,还是要画图才来的直观,字节码,操作数栈,局部变量表。

public class _3SynchronizedTest {

    private int i = 0;
    public  void add(){
        i++;
    }


    private Object obj = new Object();
    public void subtract(){

        // obj 就是同步监视器
        synchronized (obj){
            i--;
        }
    }
}
 0 aload_0 // 加载局部变量表中的索引下标为0的变量(this)的地址值,加入到操作数栈当中
 1 getfield #4 <com/atguigu/java1/_3SynchronizedTest.obj> // 获取对象的某个字段的地址值,加入到操作数栈当中
 4 dup // 复制一份
 5 astore_1 // 弹出一个对象的地址值,放到局部变量表索引为1的位置
 6 monitorenter // 将锁对象的同步监视器的计数器的值从0改成1
 7 aload_0 // 把this的地址值拿过来,放到操作数栈当中
 8 dup // 然后将操作数栈顶上的元素的地址值复制一份
 9 getfield #2 <com/atguigu/java1/_3SynchronizedTest.i>
12 iconst_1
13 isub
14 putfield #2 <com/atguigu/java1/_3SynchronizedTest.i>
17 aload_1
18 monitorexit
19 goto 27 (+8)
22 astore_2
23 aload_1
24 monitorexit
25 aload_2
26 athrow
27 return
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值