java字节码指令

java字节码指令

   概要

   众所周知,Java 字节码是跨平台的,因此 Java 才能一次编译,处处运行

   关于JVM和字节码:

   1. JVM:  JVM(Java Virtual Machine,Java虚拟机)是Java程序运行的虚拟计算机。它是Java平台的一部分,负责解释和执行Java字节码,并提供一种跨平台的运行环境,使得Java程序可以在不同的操作系统上运行,实现“一次编译,处处运行”的特性。
   2. 字节码:在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。

   一、为什么要学习字节码

   对学习技术来说,如果基础不扎实,就很难从专业底层的角度思考问题的本质。同样,如果不学习字节码,就很难从字节码层面去分析和理解问题。

   通过对字节码的学习可以帮助我们可以更好地理解 Java中各种语法和语法糖背后的原理,更好地理解多态等语言特性。  

   下面结合一些例子分别说明各个指令的作用

   二、示例

   1. 字符串对象的创建

   String s1 = new String("abc");

    1) new

   堆中创建对象,此时未被初始化
    2)dup

    对应的英文单词为dump,表示复制、拷贝

    作用:复制栈顶的数值并将其压入栈顶两次
    3)ldc

    用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中。
    4)invokespecial

    用于调用对象的实例初始化方法、私有方法或父类方法

  • 调用构造方法,用于初始化对象的实例。
  • 调用私有方法,因为私有方法在同一类内是可见的。
  • 调用父类的方法,即调用超类中的方法。

   5)astore_1

   将栈顶引用类型数值保存到局部变量表中的第1个槽位
   6)return

   返回方法并将返回值传递给调用者

   2. 整型对象的创建以及比较

1     public static void main(String[] args) {
2         Integer i1 = 56;
3         Integer i2 = 56;
4         Integer i3 = 129;
5         Integer i4 = 129;
6         System.out.println(i1 == i2);
7         System.out.println(i3 == i4);
8     }

   下面是各个字节码指令的分析:

 1 0  bipush 56         // 将整数值56推送到栈顶
 2 2  invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
 3 5  astore_1          // 将栈顶整数值存储到局部变量1(i1)
 4 6  bipush 56         // 将整数值56推送到栈顶
 5 8  invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
 6 11 astore_2          // 将栈顶整数值存储到局部变量2(i2)
 7 12 sipush 129        // 将短整数值129推送到栈顶
 8 15 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
 9 18 astore_3          // 将栈顶整数值存储到局部变量3(i3)
10 19 sipush 129        // 将短整数值129推送到栈顶
11 22 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
12 25 astore 4          // 将栈顶整数值存储到局部变量4(i4)
13 27 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
14 30 aload_1           // 将局部变量1(i1)推送到栈顶
15 31 aload_2           // 将局部变量2(i2)推送到栈顶
16 32 if_acmpne 39 (+7) // 如果i1和i2不相等,则跳转到39(输出1),其中(+7) 表示跳转的偏移量为7个字节,因为一条字节码指令通常是1个字节,所以这里的偏移量是7个字节。
17 35 iconst_1          // 推送整数常量1到栈顶(表示真)
18 36 goto 40 (+4)      // 无条件跳转到40
19 39 iconst_0          // 推送整数常量0到栈顶(表示假)
20 40 invokevirtual #4 <java/io/PrintStream.println : (Z)V>  // 打印栈顶整数值
21 43 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
22 46 aload_3           // 将局部变量3(i3)推送到栈顶
23 47 aload 4           // 将局部变量4(i4)推送到栈顶
24 49 if_acmpne 56 (+7) // 如果i3和i4不相等,则跳转到56(输出1)
25 52 iconst_1          // 推送整数常量1到栈顶(表示真)
26 53 goto 57 (+4)      // 无条件跳转到57
27 56 iconst_0          // 推送整数常量0到栈顶(表示假)
28 57 invokevirtual #4 <java/io/PrintStream.println : (Z)V>  // 打印栈顶整数值
29 60 return            // 返回

     三、字节码指令

     1.  数据类型

      x 为操作码助记符,表明是哪一种数据类型。见下表所示:

     

     2. 加载与存储指令

     加载(load)和存储(store)指令是使用最频繁的指令,用于将数据从栈帧的局部变量表和操作数栈之间来回传递。

     1)将局部变量表中的变量压入操作数栈

     xload_<n>(x 为 i、l、f、d、a,n 默认为 0 到 3),表示将第 n 个局部变量压入操作数栈中。

     xload(x 为 i、l、f、d、a),通过指定参数的形式,将局部变量压入操作数栈中,当使用这个指令时,表示局部变量的数量可能超过了 4 个。

     举个例子,如下图:

    iload_1:将局部变量表中下标为 1 的 int 变量压入操作数栈中。

    aload_2:将局部变量表中下标为 2 的引用数据类型变量(此时为 String)压入操作数栈中。

    lload_3:将局部变量表中下标为 3 的 long 型变量压入操作数栈中。

    iload 5:将局部变量表中下标为 5 的 int 变量(实际为 boolean)压入操作数栈中。

    通过查看局部变量表就能关联上了。

    2)将常量池中的常量压入操作数栈中

    根据数据类型和入栈内容的不同,又可以细分为 const 系列、push 系列和 Idc 指令。

    const 系列,用于特殊的常量入栈,要入栈的常量隐含在指令本身。

   

    push 系列,主要包括 bipush 和 sipush,前者接收 8 位整数作为参数,后者接收 16 位整数。

    Idc 指令,当 const 和 push 不能满足的时候,万能的 Idc 指令就上场了,它接收一个 8 位的参数,指向常量池中的索引。

    Idc_w:接收两个 8 位数,索引范围更大。

    如果参数是 long 或者 double,使用 Idc2_w 指令。

    举个例子,如下图:

     

    iconst_m1:将 -1 入栈。范围 [-1,5]。
    bipush 127:将 127 入栈。范围 [-128,127]。
    sipush 32767:将 32767 入栈。范围 [-32768,32767]。  
    ldc #6 <32768>:将常量池中下标为 6 的常量 32768 入栈。
    aconst_null:将 null 入栈。
    ldc #7 <沉默王二>:将常量池中下标为 7 的常量“沉默王二”入栈。

    3)将栈顶的数据出栈并装入局部变量表中

    主要是用来给局部变量赋值,这类指令主要以 store 的形式存在。

  •     xstore_<n>(x 为 i、l、f、d、a,n 默认为 0 到 3)
  •     xstore(x 为 i、l、f、d、a)

    明白了 xload_<n> 和 xload,再看 xstore_<n> 和 xstore 就会轻松得多,作用反了一下而已。

    xstore_<n> 和 xstore n 的区别在于,前者相当于只有操作码,占用 1 个字节;后者相当于由操作码和操作数组成,操作码占 1 个字节,操作数占 1 个字节,一共占 2 个字节。

    说明:一般说来,类似像store这样的命令需要带一个参数,用于指明将弹出的元素放在局部变量表的第几个位置。但是,为了尽可能压缩指令大小,使用专门的 istore_1 指令表示将弹出的元素放置在局部变量表第1个位置。类似的还有istore_0,istore_2,istore_3,它们分别表示从操作数栈栈顶弹出一个元素,存放在局部变量表第0,2,3个位置。由于局部变量表前几个位置总是非常常用,因此这种做法虽然增加了指令数量,但是可以大大压缩生成的字节码的体积。如果局部变量表很大,需要存储的槽位大于3,那么可以使用istore指令,外加一个参数,用来表示需要存放的槽位位置。

    举个例子,如下图:

   

    istore_3:从操作数中弹出一个整数,并把它赋值给局部变量表中索引为 3 的变量。

    astore 4:从操作数中弹出一个引用数据类型,并把它赋值给局部变量表中索引为 4 的变量。

    通过查看局部变量表就能关联上了。如下图:

 

    4)条件判断指令

 1 ifeq   当栈顶int类型元素    等于0时,跳转
 2                             
 3 ifne   当栈顶int类型元素    不等于0时,跳转
 4                             
 5 iflt   当栈顶int类型元素    小于0时,跳转
 6                             
 7 ifle   当栈顶int类型元素    小于等于0时,跳转
 8                             
 9 ifgt   当栈顶int类型元素    大于0时,跳转
10                             
11 ifge   当栈顶int类型元素    大于等于0时,跳转
12 
13
14 // int类型 和 reference  当然也有对两个操作数的比较指令,而且还一步到位了
15                             
16 if_icmpeq    比较栈顶两个int类型数值的大小 ,当前者 等于后者时,跳转
17                             
18 if_icmpne    比较栈顶两个int类型数值的大小 ,当前者不等于后者时,跳转
19                             
20 if_icmplt    比较栈顶两个int类型数值的大小 ,当前者小于后者时,跳转
21                             
22 if_icmple    比较栈顶两个int类型数值的大小 ,当前者小于等于后者时,跳转
23                             
24 if_icmpge    比较栈顶两个int类型数值的大小 ,当前者大于等于后者时,跳转
25                             
26 if_icmpgt    比较栈顶两个int类型数值的大小 ,当前者大于后者时,跳转
27                             
28 if_acmpeq    比较栈顶两个引用类型数值的大小 ,当前者等于后者时,跳转
29                             
30 if_acmpne    比较栈顶两个引用类型数值的大小 ,当前者不等于后者时,跳转

     这里面还涉及的指令有:

     条件分支指令,它用于比较引用类型(对象)是否不相等 

     1)  invokestatic

     用于调用静态方法

     2)  getstatic

     用于获取静态字段的值

     3)goto

     用于无条件跳转到指定的目标位置

     说明:这种无条件跳转的目的可能是为了实现循环、条件分支等控制结构

  

     参考链接:

     https://cloud.tencent.com/developer/article/1870150

     https://cloud.tencent.com/developer/article/1333540

posted @ 2024-02-03 11:55  欢乐豆123  阅读(9)  评论(0编辑  收藏  举报