使用Java字节码分析四则运算

下面展示了一个简单的 Java类,进行了四则运算。


public class Hello{

	public static void main(String[] args){
		int a = 1;
		int b = 2 + a;
		int c = 3 * b;
		int d = c - a;
		float e = d / 2f;
		
		System.out.println(e);
	}
}

通过命令 javac -g Hello.java 进行编译,然后通过命令 java Hello 运行编译后的代码,得到结果是 4.0

接着通过命令 javap -c -verbose Hello 进行反编译,输出结果如下:

Classfile /D:/Hello.class
  Last modified 2021-6-19; size 604 bytes
  MD5 checksum 2529ef11eea3e7947cfc9553f36bda77
  Compiled from "Hello.java"
public class Hello
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#26         // java/lang/Object."<init>":()V
   #2 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #29.#30        // java/io/PrintStream.println:(F)V
   #4 = Class              #31            // Hello
   #5 = Class              #32            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               LHello;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               c
  #21 = Utf8               d
  #22 = Utf8               e
  #23 = Utf8               F
  #24 = Utf8               SourceFile
  #25 = Utf8               Hello.java
  #26 = NameAndType        #6:#7          // "<init>":()V
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(F)V
  #31 = Utf8               Hello
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (F)V
{
  public Hello();
    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 2: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LHello;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=6, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: iload_1
         4: iadd
         5: istore_2
         6: iconst_3
         7: iload_2
         8: imul
         9: istore_3
        10: iload_3
        11: iload_1
        12: isub
        13: istore        4
        15: iload         4
        17: i2f
        18: fconst_2
        19: fdiv
        20: fstore        5
        22: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        25: fload         5
        27: invokevirtual #3                  // Method java/io/PrintStream.println:(F)V
        30: return
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 6
        line 8: 10
        line 9: 15
        line 11: 22
        line 12: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            2      29     1     a   I
            6      25     2     b   I
           10      21     3     c   I
           15      16     4     d   I
           22       9     5     e   F

在编译时加上了 -g 参数,是为了生成局部变量表 LocalVariableTable ;在反编译时加上了 -verbose 是为了输出附件信息。 在上面的例子中反编译输出的结果主要包括:类的来源,校验和,版本号,常量池,构造函数,main函数。

尝试对其中的信息进行分析:

Classfile /D:/Hello.class   //描述了文件来源
  Last modified 2021-6-19; size 604 bytes //修改信息,文件大小
  MD5 checksum 2529ef11eea3e7947cfc9553f36bda77    // MD5 校验和
  Compiled from "Hello.java"  // 对哪个类进行反编译
public class Hello
  minor version: 0
  major version: 52   // java 版本号
  flags: ACC_PUBLIC, ACC_SUPER  // 该类是 public
Constant pool:  // 常量池
   #1 = Methodref          #5.#26         // java/lang/Object."<init>":()V
   #2 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #29.#30        // java/io/PrintStream.println:(F)V
   #4 = Class              #31            // Hello
   #5 = Class              #32            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               LHello;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               c
  #21 = Utf8               d
  #22 = Utf8               e
  #23 = Utf8               F
  #24 = Utf8               SourceFile
  #25 = Utf8               Hello.java
  #26 = NameAndType        #6:#7          // "<init>":()V
  #27 = Class              #33            // java/lang/System
  #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #29 = Class              #36            // java/io/PrintStream
  #30 = NameAndType        #37:#38        // println:(F)V
  #31 = Utf8               Hello
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (F)V
{
  public Hello(); //构造器
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1 //构造器函数 使用的栈深度是1,用于存放this指针
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V 
         4: return
      LineNumberTable: //行号表:java源文件行号与字节码文件偏移量之间的对应关系
        line 2: 0
      LocalVariableTable: // 局部变量表:在一个方法中用到的变量
        Start  Length  Slot  Name   Signature
            0       5     0  this   LHello;

  public static void main(java.lang.String[]); // main 方法
    descriptor: ([Ljava/lang/String;)V  //方法描述:是一个String对象的数组
    flags: ACC_PUBLIC, ACC_STATIC   // 方法是 public static
    Code:
      stack=2, locals=6, args_size=1 // 当前方法栈深度是2,有6个变量,1个入参
         0: iconst_1  // 常量值1放到栈
         1: istore_1  // 将栈顶值放到 局部变量表中的1号槽位
         2: iconst_2  // 常量值2放到栈
         3: iload_1   // 将局部变量表中的1号槽位的值放到栈顶
         4: iadd      //执行一次加法操作
         5: istore_2  //将栈顶值放到 局部变量表中的2号槽位
         6: iconst_3  // 常量值3放到栈
         7: iload_2    // 将局部变量表中的2号槽位的值放到栈顶
         8: imul       //执行一次乘法操作
         9: istore_3   // 将栈顶值放到 局部变量表中的3号槽位
        10: iload_3    // 将局部变量表中的3号槽位的值放到栈顶
        11: iload_1    // 将局部变量表中的1号槽位的值放到栈顶
        12: isub       //执行一次减法操作
        13: istore        4   // 将栈顶一个int类型的值放到 局部变量表中的4号槽位
        15: iload         4   // 将局部变量表中的4号槽位的值放到栈顶
        17: i2f      //int 类型转换成 float类型
        18: fconst_2   // 一个float类型的常量值2放到栈
        19: fdiv       //执行一次除法操作
        20: fstore        5  // 将栈顶一个float类型的值放到 局部变量表中的5号槽位
        22: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        25: fload         5
        27: invokevirtual #3                  // Method java/io/PrintStream.println:(F)V //方法调用
        30: return      //返回
      LineNumberTable: //行号表:java源文件行号与字节码文件偏移量之间的对应关系
        line 5: 0     // 源文件中表示 int a = 1;
        line 6: 2
        line 7: 6
        line 8: 10
        line 9: 15
        line 11: 22
        line 12: 30
      LocalVariableTable: // 局部变量表:在一个方法中用到的变量
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String; // 0号槽位对应变量args, 类型是Stirng[]
            2      29     1     a   I    // 1号槽位对应变量a, 类型是Integer
            6      25     2     b   I    // 2号槽位对应变量b, 类型是Integer
           10      21     3     c   I    // 3号槽位对应变量c, 类型是Integer
           15      16     4     d   I    // 4号槽位对应变量d, 类型是Integer
           22       9     5     e   F    // 5号槽位对应变量f, 类型是Float

在上面对其中的信息进行了注释,相信能够看的明白各个助记符的含义。

文章到这里,都还没有介绍 Java字节码,因为我想先通过实际的用例来说明Java字节码的含义。Java bytecode 由单字节( byte )的指令组成, 理论上最多支持 256 个操作码(opcode)。实际上Java只使用了200左右的操作码, 还有一些操作码则保留给调试操作。

操作码, 或者称为 指令 ,主要由 类型前缀 操作名称 两部分组成。 例如, i 前缀代表 integer ,所以,iadd 表示对整数执行加法运算。i2f 表示 int类型转换成 float类型。 fdiv 表示对浮点数执行除法运算。

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

  • 栈操作指令,包括与局部变量交互的指令
  • 程序流程控制指令
  • 对象操作指令,包括方法调用指令
  • 算术运算以及类型转换指令

在上面给的例子中,可以看到四则运算的过程有很多的操作,因为JVM是一台基于栈的计算机器。每个线程都有一个独属于自己的线程栈(JVM stack), 用于存储 栈帧 (Frame)。每一次方法调用,JVM都会自动创建一个栈帧。 栈帧 由 操作数栈 , 局部变量表 以及一个 class指针 组成。 class指针 指向当前方法 在运行时常量池中对应的class。用一个图说明他们之间的关系。

操作数栈和局部变量表之间频繁使用的指令是 storeload

刚才提到了JVM是一台基于栈的计算机器,现在用一个简单的示例看一下计算过程。

public class Hello{

	public static void main(String[] args){
		int a = 4;
		int b = 5;
		int c = a + b;

		System.out.println(c);
	}
}

编译 :javac -g Hello.java

运行: java Hello

反编译: javap -c -verbose Hello (下面只是展示了main函数中的计算过程)

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_4
         1: istore_1
         2: iconst_5
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: istore_3
         8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_3
        12: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        15: return
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 4
        line 9: 8
        line 10: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
            2      14     1     a   I
            4      12     2     b   I
            8       8     3     c   I

对上面的指令步步解析:

首先是 ` 0: iconst_4 ,生成一个整数类型的常量值 4` 并放到栈顶。

然后是 ` 1: istore_1 ,将栈顶的整数值存储到局部变量表中1号槽位,即 a=4`。

接着是 ` 2: iconst_5 ,生成一个整数类型的常量值 5` 并放到栈顶。

然后是 ` 3: istore_2 ,将栈顶的整数值存储到局部变量表中2号槽位,即 b=4`。

接着是 ` 4: iload_1 ,将局部变量表中1`号槽位的值加载到栈顶。

接着是 ` 5: iload_2 ,将局部变量表中2`号槽位的值加载到栈顶。

接着是 ` 6: iadd` ,执行一次加法运算。

接着是 ` 7: istore_3 ,将栈顶的整数值存储到局部变量表中3号槽位,即 c=9`。

接着是 ` 8: getstatic ,获取静态字段,即此时执行了 System.out`。

接着是 ` 11: iload_3 ,将局部变量表中3`号槽位的值加载到栈顶。

接着 ` 12: invokevirtual ,执行方法调用,即此时执行了 out.println(c)`。

最后是 ` 15: return` ,方法返回。

最后,从上面的分析过程,我们还可以看到:一个简单的加法操作是需要三个指令才能完成的:int c = a + b

         4: iload_1
         5: iload_2
         6: iadd