中:第2章 字节码指令集与解析举例

本文深入剖析了Java虚拟机(JVM)字节码指令,包括加载与存储、算术运算、类型转换、对象创建与访问、方法调用与返回等多个方面。通过实例代码和字节码分析,详细阐述了i++和++i的区别,以及异常处理和同步控制等关键概念,帮助读者理解JVM底层运行机制。
摘要由CSDN通过智能技术生成

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


01-概述

执行模型

在这里插入图片描述

在这里插入图片描述

字节码与数据类型

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

指令分析

在这里插入图片描述

02-加载与存储指令

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

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

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

在这里插入图片描述

这里为了演示方便,局部变量表,用变量名表示的,实际上存储的是实际的值。

2-常量入栈指令

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

注意:常量入栈指令中的n和局部变量压栈指令中的n不一样,本次的n代表数值或者对象,而不是局部变量表中的下标

  public void pushConstLdc() {
        int i = -1;
        int a = 5;
        int b = 6;
        int c = 127;
        int d = 128;
        int e = 32767;
        int f = 32768;
    }

在这里插入图片描述

 public void constLdc() {
        long a1 = 1;
        long a2 = 2;
        float b1 = 2;
        float b2 = 3;
        double c1 = 1;
        double c2 = 2;
        Date d = null;

    }

在这里插入图片描述

在这里插入图片描述

3-出栈入局部变量表指令

举例分析如下:

public void store(int k, double d) {
        int m = k + 2;
        long l = 12;
        String str = "atguigu";
        float f = 10.0F;
        d = 10;
    }

在这里插入图片描述
里面有代码,也有字节码,所以可以根据老师给的图展开分析,首先该方法被调用的时候,形式参数k和d都是有确定的值,由于该方法不是静态方法,所以局部变量表中的第一个位置(槽位)存储this,而第二个位置存储k具体的值,由于老师只是分析,没有调用这个方法,所以老师全部使用的变量名称来代替具体的值,所以明白就好,继续来分析,然后第三个和第四个位置储存d具体的值,由于d是double类型,所以需要占据两个槽位,数据已经准备好了,那就来看字节码,首先iload_1是将局部变量表中下标为1的k值取出来压入操作数栈中,然后iconst_2是将常量池中的整型值2压入操作数栈,iadd让操作数栈弹出的k值和整型值2执行相加操作,之后将相加的结果值m压入操作数栈中,请注意老师的画法,在执行弹栈和压栈操作之后,老师并没有删除操作数栈中的k值和2,这是因为老师让我们知道具体的操作过程,所以故意为之,不过真正的操作是弹栈之后k值和2就会从操作数栈中弹出,之后操作数栈中就没有k值和2了,只有m值了,然后istore_4是将操作数栈中的m值弹出栈,然后放在局部变量表中下标为4的位置,idc2_w #13<12>代表将long型值12压入操作数栈,istore5是将值12弹栈之后放入局部变量表中下标为5的位置,由于12是long型,所以占据两个位置(槽位),ldc #15代表将字符串atguigu压入操作数栈,astore 7代表将字符串atguigu弹栈之后放入局部变量表中下标为7的位置,idc #16<10.0>代表将float类型数据10.0压入操作数栈,fstore 8代表将10.0弹出栈,然后放入局部变量表中下标为8的位置,idc2_w #17<10.0>代表将10.0压入操作数栈,dstore2代表将10.0弹出栈,之后将10.0放入下标为2和3的操作,毕竟这是double类型数据

槽位复用:

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

在这里插入图片描述

03-算数指令

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

所有算术指令

在这里插入图片描述

举例

在这里插入图片描述

一个曾经的案例1

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

一个曾经的案例2

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

注意:println()方法的本地变量表中会放55,这样该方法就可以使用了

i++和++i

代码1:

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

代码1的字节码:

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

用javap -v 类名.class 打开字节码文件,找到method 6 ()方法:

 public void method6();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=1
         0: bipush        10
         2: istore_1
         3: iinc          1, 1
         6: return
      LineNumberTable:
        line 48: 0
        line 49: 3
        line 53: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/atguigu/java/ArithmeticTest;
            3       4     1     i   I

在这里插入图片描述

我们一行行解析字节码:

  • 0 bipush 10 :把 10 放进操作数栈中
    在这里插入图片描述

  • istore_1:取出操作数栈中的10 放到局部变量表索引为1的位置(因为这个是非静态的方法,局部变量表索引为0 的位置放的是this)
    在这里插入图片描述

  • iinc 1 by 1:把局部变量表中索引为1的位置加1。

  • 在这里插入图片描述
    所以这行指令执行完之后,这个索引为1的位置就是11了。

  • return:因为是void类型的方法,因此这里直接return (注意void方法也是有返回指令的)

因此代码1 如果打印i的话:

 public void method6(){
        int i = 10;
        i++;
        System.out.println(i);//10
    }
 0 bipush 10
 2 istore_1
 3 iinc 1 by 1
 6 getstatic #2 <java/lang/System.out>
 9 iload_1
10 invokevirtual #5 <java/io/PrintStream.println>
13 return

6 getstatic #2 <java/lang/System.out>:System.out获取静态变量System.out
9 iload_1:把局部变量表的索引为1的【10】放到操作数栈中
10 invokevirtual #5 <java/io/PrintStream.println>:调用println方法
因此输出结果就是10.

代码2:
用与代码1同样的方式分析,我这里就不像代码1那么具体了,方法是一样的。

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

代码2的字节码

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

你会发现,它与代码1的字节码是一样的!这里仅仅是因为,我们没有只是简单的i++和++i,而没有进行其它的操作,所以,本质山i++和++i如果不做其他的赋值操作的话,这俩其实是效果等价的

这也就解释了 for(int i =0;i<10;i++) 和 for(int i =0;i<10;++i)其实是一样的

所以这个i如果打印一下,其实也是在索引为1的取索引为1的位置上的【10】,这里我们就不再演示了,感兴趣的同学,可以验证一下。

代码 3:

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

        int j = 20;
        int b = ++j;
    }
 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

 public void method7();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=5, args_size=1
         0: bipush        10
         2: istore_1
         3: iload_1
         4: iinc          1, 1
         7: istore_2
         8: bipush        20
        10: istore_3
        11: iinc          3, 1
        14: iload_3
        15: istore        4
        17: return
      LineNumberTable:
        line 55: 0
        line 56: 3
        line 58: 8
        line 59: 11
        line 60: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  this   Lcom/atguigu/java/ArithmeticTest;
            3      15     1     i   I
            8      10     2     a   I
           11       7     3     j   I
           17       1     4     b   I

  • 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 方法结束

最终我们局部变量表是这样子的:

在这里插入图片描述
把结果输出一下看看啥样子

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

        int j = 20;
        int b = ++j;
        System.out.println(a);
        System.out.println(i);
        System.out.println(b);
        System.out.println(j);
    }

结果

10
11
21
21

字节码:

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 //0到15 与上面一样,下面才是表示输出的字节码
17 getstatic #2 <java/lang/System.out>
20 iload_2
21 invokevirtual #5 <java/io/PrintStream.println>
24 getstatic #2 <java/lang/System.out>
27 iload_1
28 invokevirtual #5 <java/io/PrintStream.println>
31 getstatic #2 <java/lang/System.out>
34 iload 4
36 invokevirtual #5 <java/io/PrintStream.println>
39 getstatic #2 <java/lang/System.out>
42 iload_3
43 invokevirtual #5 <java/io/PrintStream.println>
46 return

有字节码分析可以知道

  • System.out.println(a)的字节码:

    17 getstatic #2 <java/lang/System.out>
    20 iload_2
    21 invokevirtual #5 <java/io/PrintStream.println>

往操作数栈加载是上图索引为2的【10】。所以a输出的结果是10

  • System.out.println(i);

    24 getstatic #2 <java/lang/System.out>
    27 iload_1
    28 invokevirtual #5 <java/io/PrintStream.println>

    往操作数栈加载是上图索引为1的【11】。所以i输出的结果是11

  • System.out.println(b);

    31 getstatic #2 <java/lang/System.out>
    34 iload 4
    36 invokevirtual #5 <java/io/PrintStream.println>

    往操作数栈加载是上图索引为4的【21】。所以i输出的结果是21

  • System.out.println(j);

    39 getstatic #2 <java/lang/System.out>
    42 iload_3
    43 invokevirtual #5 <java/io/PrintStream.println>

    往操作数栈加载是上图索引为3的【21】。所以i输出的结果是21

所以现在你应该体会到了为什么说i++是先赋值再加加,而++i 是先加加再赋值了吧,一定要注意这个口令不是对所有的场景都适用,比如我们一开始举的例子代码1和代码2。

下面来点有意思的:
代码4:

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

这个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


这个我就不画图针对字节码一行一行的解释了。
这里面istore_1两次,后面操作栈中的10把之前局部变量表的11覆盖了,所以打印就是10了,你可以按照我上面的方法,尝试画一下流程,授人以鱼不如授人以渔。

再改一下上面代码:
代码5:

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

这个i打印出来就是11
对比一下上面两个字节码,其实最大的不同在于:
代码4是:
3 iload_1
4 iinc 1 by 1
而代码5是:
3 iinc 1 by 1
6 iload_1
这就造成了代码4 加载进操作数栈的是10,代码5 是11,这就造成了再次istore_1时,前者覆盖是10覆盖11,后者是11覆盖11。

比较指令

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

04-类型转换指令

在这里插入图片描述

1-宽化类型转换

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

2-窄化类型转换

在这里插入图片描述

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

05-对象的创建与访问指令

在这里插入图片描述

1-创建指令

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

2-字段访问指令

在这里插入图片描述
在这里插入图片描述
注意:get是入栈,而put是出栈
举例:
在这里插入图片描述

3-数组操作指令

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

4-类型检查指令

在这里插入图片描述

06-方法调用与返回指令

1-方法调用指令

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

2-方法返回指令

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

07-操作数栈管理指令

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

08-控制转义指令

在这里插入图片描述

1-条件跳转指令

在这里插入图片描述
在这里插入图片描述
注意:
1、对于float、double、long类型的比较,它们比较之后生成的是int类型的0、1、-1,这个过程可以使用比较指令和条件跳转指令来完成,虽然得到的是int类型的值,但是System.out.println(XXX)中的值是布尔类型,你可以在jclasslib中的常量池信息中看到写的是Z,代表布尔值类型
2、int类型值(包含byte、char、short)比较 和 对象类型值比较需要使用比较条件跳转指令

2-比较条件跳转指令

在这里插入图片描述
注意:
1、上面所说的后者是栈顶元素,而前者是栈顶下面的元素
2、对于float、double、long类型的比较,它们比较之后生成的是int类型的0、1、-1,这个过程可以使用比较指令和条件跳转指令来完成
而 int类型值(包含byte、char、short)比较 和 对象类型值比较需要使用比较条件跳转指令,其中对象类型值不是比较的地址,就是比较对象中的某些字段值,这又归咎到float、double、long、int类型的比较中了
3、无论哪种比较,也不管两个比较值的中间是什么符号(>、<、>=、<=等等),始终都是栈顶下部元素 比较符 栈顶元素,这是不会改变的,然后结合比较符得出结果,如果是true,那就跳转,否则不跳转继续往下执行

3-多条件分支跳转

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

4-无条件跳转

在这里插入图片描述

09-异常处理指令

1-抛出异常指令

在这里插入图片描述
注意:
如果使用throw new 异常名称()这种形式来抛出异常,那就会在代码中出现athrow指令,而在方法上面添加throw 异常名称这种形式来抛出异常,然后使用jclasslib的时候就会出现在方法下面多出现一个属性Exceptions,如下图所示:
在这里插入图片描述

2-异常处理与异常表

在这里插入图片描述
异常表如下所示:
在这里插入图片描述
异常表的含义是如果在Start PC和End PC之间(大于等于Start PC,小于End PC)出现对应的Catch Type异常问题(出现异常就匹配对应的异常),将会在操作数栈中压入相应的异常类对象,之后跳转到Handler PC的位置去执行对应的字节码指令
注意:
当异常出现的时候也会压入操作数栈,之后还会存储局部变量表中

10-同步控制指令

在这里插入图片描述

1-方法级的同步

同步方法(添加synchronized的方法):
在这里插入图片描述
在这里插入图片描述
注意:
一个方法无论是否添加synchronized,你都无法在字节码中看出区别,例如:
在这里插入图片描述
是否是同步方法在字节码文件中你是无法看出区别的,但是可以在方法访问标识中看出区别

2-方法内指令指令序列的同步

同步代码块:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例子:
在这里插入图片描述
操作数栈中的对象和monitorenter结合起来可以让线程获取锁,做法就是让对象的监视器标记从0变成1,这就代表该线程上锁了,然后在操作数栈的aload_1和monitorexit结合起来就可以让线程解锁,做法就是让对象的监视器标记从1变成0,这个解锁需要在方法退出之前完成,如果方法执行过程中出现了任何异常,将会跳到异常处理的字节码处执行相关代码,如果异常处理的字节码部分出现了问题,那就重新执行异常处理的字节码,这些内容都在异常表中写的很明确,其中异常表也在上面截图中

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值