JVM字节码


一、简单字节码分析

  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."":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class HelloByteCode
       3: dup
       4: invokespecial #3                  // Method "":()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."":()V
   #2 = Class              #14            // HelloByteCode
   #3 = Methodref          #2.#13         // HelloByteCode."":()V
   #4 = Class              #15            // java/lang/Object
   #5 = Utf8               
   #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          // "":()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."":()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 "":()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."":()V
   #2 = Class              #20            // HelloByteCode
   #3 = Methodref          #2.#19         // HelloByteCode."":()V
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               
   #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          // "":()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."":()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 "":()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."":()V
   #2 = Class              #29            // MovingAverage
   #3 = Methodref          #2.#28         // MovingAverage."":()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               
   #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          // "":()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."":()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."":()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."":()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 表达式的实现基础。