• JVM字节码 lcl


    一、简单字节码分析

      JavaByte由单个字节(byte)的指令组成,理论上最多支持256个操作码,实际上Java只使用了200左右的操作码,还有一些操作码留给调试操作。

      根据指令的性质,主要分为四大类:

        1、栈操作指令,包括与局部变量交互的指令;JVM就类似一个计算机,计算机的运行有基于栈的、有基于寄存器的,而JVM就是基于栈的操作,因此会存在栈操作指令。

        2、程序流程控制指令,例如代码中的 if、for 等

        3、对象操作指令,包括方法调用指令;Java本身是面向对象的,其会调用对象的方法和属性,因此会存在对象操作指令。

        4、算术运算以及类型转换指令

       其中栈操作指令是虚拟机本身结构需要的,而另外三个是和Java语言对应的。

      如下一段简单的代码:

    public class HelloByteCode {
        public static void main(String[] args) {
            HelloByteCode obj = new HelloByteCode();
        }
    }

      查看其字节码

    # 编译
    javac HelloByteCode.java
    # 查看内部字节码内容
    javap -c HelloByteCode.class
    # 内容
    Compiled from "HelloByteCode.java"
    public class HelloByteCode {
      public HelloByteCode();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: new           #2                  // class HelloByteCode
           3: dup
           4: invokespecial #3                  // Method "<init>":()V
           7: astore_1
           8: return
    }

      可以分析一下上面的字节码文件,首先收两个方法,一个是类的无参构造,一个是main方法,在无参构造中,第一行,是从局部变量表下表为0的位置获取数据,load表示获取,a表示引用类型;第二行表示调用初始化方法,后面的 1 表示是对常量池中 1 所表示的常量,后面的注释说明了其调用的是其父类Object的初始化方法;第三行是返回。在main方法中,第一行表示使用常量池中2表示的常量new一个对象,注释表示了new哪个类型的对象;第二行表示压栈,即将new出来的对象压入操作数栈;第三行表示初始化,使用的是常量池中3表示的常量;第四行表示将初始化完成的对象放入局部变量表;最后返回。

      每一行的前面的数字表示操作指令偏移量,例如在构造方法中,初始偏移量为0.第二个指令的偏移量为1,说明aload指令占了一个字节码,而第三行的偏移量为4,说明初始化所占的字节码为3,除了一个字节的操作指令外,还可以有2个字节的操作数,样例中只是用了一个字节。

      上面的代码是不带常量池信息的,如果要展示常量池信息,可以在使用javap命令时加上-verbose

    # javap -c -verbose HelloByteCode.class
    Classfile ...../java-training-camp/HelloByteCode.class
      Last modified 2022-4-30; size 288 bytes
      MD5 checksum e2660631542bd407feefe48622ed4ae1
      Compiled from "HelloByteCode.java"
    public class HelloByteCode
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
       #2 = Class              #14            // HelloByteCode
       #3 = Methodref          #2.#13         // HelloByteCode."<init>":()V
       #4 = Class              #15            // java/lang/Object
       #5 = Utf8               <init>
       #6 = Utf8               ()V
       #7 = Utf8               Code
       #8 = Utf8               LineNumberTable
       #9 = Utf8               main
      #10 = Utf8               ([Ljava/lang/String;)V
      #11 = Utf8               SourceFile
      #12 = Utf8               HelloByteCode.java
      #13 = NameAndType        #5:#6          // "<init>":()V
      #14 = Utf8               HelloByteCode
      #15 = Utf8               java/lang/Object
    {
      public HelloByteCode();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 1: 0
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=2, args_size=1
             0: new           #2                  // class HelloByteCode
             3: dup
             4: invokespecial #3                  // Method "<init>":()V
             7: astore_1
             8: return
          LineNumberTable:
            line 3: 0
            line 4: 8
    }
    SourceFile: "HelloByteCode.java"

      可以看到在无参构造函数中初始化的常量1,是由常量4和常量13组成的,常量4是Object类,常量13是由常量5和常量6组成,其中注释也说明了,其是由方法名和返回参数构成的,常量5表示init方法,常量6表示返回参数,大写的V表示void。这些合起来就是常量池1的注释,调用Object类中返回值为void的init方法。而在无参构造的第二行中,invokespecial表示调用常量池1的常量,合起来,就是调用Object类中返回值为void的init方法。

      在上面的无参构造方法中,还使用LineNumberTable表示了行号,其说明偏移量为0的指令,在Java代码中的第一行,也就是生命该类的行号,在main方法中,其说明了new指令在代码的第3行,返回在代码的第4行。

      可以看到,一个class中,包含了描述信息、常量池信息、方法内容等:

        描述信息:包括类的最后修改时间、类的大小、MD5加密、编译来源、版本号(上面的版本号是52,对应JDK8,如果是51,对应JDK7,50对应JDK6)、修饰符、是否有父类等。

        常量池信息:描述了常量信息

        方法:描述了方法的修饰符、返回值、入参,还有Code码,code中有总体信息和详细信息,总体信息例如stack(执行该方法需要栈的总深度)、locals(执行该段字节码需要局部变量表的长度)、args_size(执行该段字节码需要的参数个数),详细信息就是上面描述的具体的操作指令。

      但是上面的Code码中虽然标注了有基本本地变量,但是并没有标注本地变量是哪些,其实本地变量是什么,并不会对程序的运行产生影响,如果想要展示本地变量是什么时,可以在打包时加上参数 -g 来打包即可。在下面的字节码文件中们可以看到每一个本地变量的起始位置、长度、槽位、名称、类型。默认情况下,这些信息是丢失的,如果使用常规的工具进行反编译,例如Eclipse、Idea等,反编译出来的结果一般是y1、y2、y3这样的本地变量。

    # javac -g HelloByteCode.java
    # javap -verbose HelloByteCode.class
    Classfile ....../HelloByteCode.class
      Last modified 2022-4-30; size 415 bytes
      MD5 checksum 44dd68d97fffda0bd16a524fb32b983a
      Compiled from "HelloByteCode.java"
    public class HelloByteCode
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #4.#19         // java/lang/Object."<init>":()V
       #2 = Class              #20            // HelloByteCode
       #3 = Methodref          #2.#19         // HelloByteCode."<init>":()V
       #4 = Class              #21            // java/lang/Object
       #5 = Utf8               <init>
       #6 = Utf8               ()V
       #7 = Utf8               Code
       #8 = Utf8               LineNumberTable
       #9 = Utf8               LocalVariableTable
      #10 = Utf8               this
      #11 = Utf8               LHelloByteCode;
      #12 = Utf8               main
      #13 = Utf8               ([Ljava/lang/String;)V
      #14 = Utf8               args
      #15 = Utf8               [Ljava/lang/String;
      #16 = Utf8               obj
      #17 = Utf8               SourceFile
      #18 = Utf8               HelloByteCode.java
      #19 = NameAndType        #5:#6          // "<init>":()V
      #20 = Utf8               HelloByteCode
      #21 = Utf8               java/lang/Object
    {
      public HelloByteCode();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 1: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   LHelloByteCode;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=2, args_size=1
             0: new           #2                  // class HelloByteCode
             3: dup
             4: invokespecial #3                  // Method "<init>":()V
             7: astore_1
             8: return
          LineNumberTable:
            line 3: 0
            line 4: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  args   [Ljava/lang/String;
                8       1     1   obj   LHelloByteCode;
    }
    SourceFile: "HelloByteCode.java"

      上面的是字节码文件,而实际上计算机运行使用的是二进制文件,在运行时,JVM将助记符变为16进制运行的,以main方法为例,在0的位置是new,在3的位置是dup,然后new后面可以有两个字节的操作数,但是字节码中new后面只有一个2,那么将2号下标职位02,1号下标补0,同理,初始化指令后面也可以有两个操作数,但是字节码后面只有一个3,因此在下标6的位置置位03,前面补位0,最终就形成了一个类似于16进制的code码,而这些操作指令都是助记符,其对应都有相应的16进制,例如new对应bb,也就是187,同理,其余的助记符都有其对应的16进制,可以使用一些支持16进制的文本编辑器打开字节码文件,就可以看到其真实对应的16进制内容。

            

     二、四则运算字节码

    public class MovingAverage {
        private int count = 0;
        private double sum = 0.0D;
    
        public void submit(double value) {
            this.count++;
            this.sum += value;
        }
    
        public double getAvg(){
            if(this.count == 0){
                return sum;
            }
            return this.sum/this.count;
        }
    }
    
    public class LocalVariableTest {
        public static void main(String[] args) {
            MovingAverage ma = new MovingAverage();
            int num1 = 1;
            int num2 = 2;
            ma.submit(num1);
            ma.submit(num2);
            double avg = ma.getAvg();
        }
    }

      对上面的代码进行编译,然后查看LocalVariableTest的字节码文件

    # javac -g MovingAverage.java
    # javac -g LocalVariableTest.java
    # javap -verbose LocalVariableTest.class
    Classfile ....../LocalVariableTest.class
      Last modified 2022-4-30; size 614 bytes
      MD5 checksum eb6bb9c0d6a1a6f0027bd30f3f6b1a6d
      Compiled from "LocalVariableTest.java"
    public class LocalVariableTest
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #7.#28         // java/lang/Object."<init>":()V
       #2 = Class              #29            // MovingAverage
       #3 = Methodref          #2.#28         // MovingAverage."<init>":()V
       #4 = Methodref          #2.#30         // MovingAverage.submit:(D)V
       #5 = Methodref          #2.#31         // MovingAverage.getAvg:()D
       #6 = Class              #32            // LocalVariableTest
       #7 = Class              #33            // java/lang/Object
       #8 = Utf8               <init>
       #9 = Utf8               ()V
      #10 = Utf8               Code
      #11 = Utf8               LineNumberTable
      #12 = Utf8               LocalVariableTable
      #13 = Utf8               this
      #14 = Utf8               LLocalVariableTest;
      #15 = Utf8               main
      #16 = Utf8               ([Ljava/lang/String;)V
      #17 = Utf8               args
      #18 = Utf8               [Ljava/lang/String;
      #19 = Utf8               ma
      #20 = Utf8               LMovingAverage;
      #21 = Utf8               num1
      #22 = Utf8               I
      #23 = Utf8               num2
      #24 = Utf8               avg
      #25 = Utf8               D
      #26 = Utf8               SourceFile
      #27 = Utf8               LocalVariableTest.java
      #28 = NameAndType        #8:#9          // "<init>":()V
      #29 = Utf8               MovingAverage
      #30 = NameAndType        #34:#35        // submit:(D)V
      #31 = NameAndType        #36:#37        // getAvg:()D
      #32 = Utf8               LocalVariableTest
      #33 = Utf8               java/lang/Object
      #34 = Utf8               submit
      #35 = Utf8               (D)V
      #36 = Utf8               getAvg
      #37 = Utf8               ()D
    {
      public LocalVariableTest();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 1: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   LLocalVariableTest;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=3, locals=6, args_size=1
             0: new           #2                  // class MovingAverage
             3: dup
             4: invokespecial #3                  // Method MovingAverage."<init>":()V
             7: astore_1
             8: iconst_1
             9: istore_2
            10: iconst_2
            11: istore_3
            12: aload_1
            13: iload_2
            14: i2d
            15: invokevirtual #4                  // Method MovingAverage.submit:(D)V
            18: aload_1
            19: iload_3
            20: i2d
            21: invokevirtual #4                  // Method MovingAverage.submit:(D)V
            24: aload_1
            25: invokevirtual #5                  // Method MovingAverage.getAvg:()D
            28: dstore        4
            30: return
          LineNumberTable:
            line 3: 0
            line 4: 8
            line 5: 10
            line 6: 12
            line 7: 18
            line 8: 24
            line 9: 30
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      31     0  args   [Ljava/lang/String;
                8      23     1    ma   LMovingAverage;
               10      21     2  num1   I
               12      19     3  num2   I
               30       1     4   avg   D
    }
    SourceFile: "LocalVariableTest.java"

      在main方法中:

        偏移量为7的操作指令为astore_1,store表示将数据加载到操作数栈中,a表示是一个引用类型,1表示本地变量1对应的内容,联合起来就是将类型为MovingAverage的引用变量ma压入本地变量表

        偏移量为8的操作指令为iconst_1,const表示将数据压栈,i表示int类型,1表示数值1,合起来就是将常量1压栈

        偏移量为9的操作指令为istrore_2,表示将其写入本地变量的下标2中,可以看到下面的本地变量表中,slot为2的位置存储的就是num1,值为1

        ......

        偏移量为12的操作指令为aload_1,表示将本地变量表中slot为1的内容进行压栈

        偏移量为14的操作指令为i2d,表示将int类型转为double类型

        ......

      对于字节码的算术操作和类型转换对比图如下所示:

            

         在上图中可以看到,i表示int、l表示long、f表示float、d表示double,另外就是a表示引用类型,在Java代码中,基本的数据类型为boolean、byte、short、int、long、double、float、char,但是在虚拟机的字节码层面,只有int、double、float、long四种,是因为java虚拟机中int是最小的处理单位,因此其将栈语言进行了简化,会将boolean、byte、short、char都转化为int表示;在上面的字节码中,可以看到很多有下划线带数字的指令,这是因为对于大多数函数来说,其需要使用的操作数栈的深度、本地变量表的个数都是比较小的,因此可以将本来需要三个字节(一个字节的操作指令和两个字节的操作数)来实现的一条指令直接使用一个字节表示的指令,节省了很大的空间,如果操作数栈比较深或者本地变量个数较多,则会直接使用类似istore、iconst这样的指令,后面跟上具体的操作数。

    三、循环字节码

    public class ForLoopTest {
        private static int[] numbers = {1,6,8};
        public static void main(String[] args) {
            MovingAverage ma = new MovingAverage();
            for(int number : numbers){
                ma.submit(number);
            }
            double avg = ma.getAvg();
        }
    }

      字节码

    # javap -verbose ForLoopTest.class
     ......public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=3, locals=6, args_size=1
             0: new           #2                  // class MovingAverage
             3: dup
             4: invokespecial #3                  // Method MovingAverage."<init>":()V
             7: astore_1
             8: getstatic     #4                  // Field numbers:[I
            11: astore_2
            12: aload_2
            13: arraylength
            14: istore_3
            15: iconst_0
            16: istore        4
            18: iload         4
            20: iload_3
            21: if_icmpge     43
            24: aload_2
            25: iload         4
            27: iaload
            28: istore        5
            30: aload_1
            31: iload         5
            33: i2d
            34: invokevirtual #5                  // Method MovingAverage.submit:(D)V
            37: iinc          4, 1
            40: goto          18
            43: aload_1
            44: invokevirtual #6                  // Method MovingAverage.getAvg:()D
            47: dstore_2
            48: return
          LineNumberTable:
            line 4: 0
            line 5: 8
            line 6: 30
            line 5: 37
            line 8: 43
            line 9: 48
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
               30       7     5 number   I
                0      49     0  args   [Ljava/lang/String;
                8      41     1    ma   LMovingAverage;
               48       1     2   avg   D
          StackMapTable: number_of_entries = 2
            frame_type = 255 /* full_frame */
              offset_delta = 18
              locals = [ class "[Ljava/lang/String;", class MovingAverage, class "[I", int, int ]
              stack = []
            frame_type = 248 /* chop */
              offset_delta = 24
    
    ......

      在main方法中出现了if_icmpge,这就是对应的for循环的判断,其中if表示判断、i表示int类型、cmp表示比较、ge表示大于或等于,该指令的意思就是前面的变量如果大于等于给定的数,就会执行偏移量为43的指令,可以看到,goto指令在43指令之前,说明其跳出了循环,如果没有跳出,则一直执行,执行到43后,goto指令表示跳转到偏移量为18的操作指令进行执行。

    四、关于方法调用

      在上面的字节码中,看到有invokevirtual,其表示方法调用,实际在JVM字节码中共有五种方法调用指令。

      Invokestatic:顾名思义,这个指令用于调用某个类的静态方法,这是方法调用指令中最快的一个。

      Invokespecial :用来调用构造函数,但也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。

      invokevirtual :如果是具体类型的目标对象,invokevirtual 用于调用公共、受保护和package 级的私有方法。

      invokeinterface :当通过接口引用来调用方法时,将会编译为 invokeinterface 指令。

      invokedynamic : JDK7 新增加的指令,是实现“动态类型语言”(Dynamically TypedLanguage)支持而进行的升级改进,同时也是 JDK8 以后支持 lambda 表达式的实现基础。

  • 相关阅读:
    HDU 1018.Big Number-Stirling(斯特林)公式 取N阶乘近似值
    牛客网 Wannafly挑战赛9 C.列一列-sscanf()函数
    牛客网 Wannafly挑战赛9 A.找一找-数据处理
    Codeforces 919 C. Seat Arrangements
    Codeforces Round #374 (Div. 2) D. Maxim and Array 线段树+贪心
    Codeforces Round #283 (Div. 2) A ,B ,C 暴力,暴力,暴力
    Codeforces Round #283 (Div. 2) E. Distributing Parts 贪心+set二分
    Codeforces Round #280 (Div. 2) E. Vanya and Field 数学
    Codeforces Round #280 (Div. 2) D. Vanya and Computer Game 数学
    Codeforces Round #280 (Div. 2) A , B , C
  • 原文地址:https://www.cnblogs.com/liconglong/p/16209454.html
Copyright © 2020-2023  润新知