java虚拟机编译器

1、编译器概览

本文关注java编译器如何把Java源代码编译为Java虚拟机指令集。主要通过举例的形式来了解编译器编译后的代码组织方式。

Java虚拟机指令将使用oracle的javap工具来生成“虚拟机汇编语言”,其实是字节码指令的一个执行清单。有其特定的格式:

<index><opcode>[<operand1><operand12...>]<comment>

index:指令操作码在数组中的下标,该数组以字节形势存储当前方法的Java虚拟机代码。
opcode:操作码。
operand:操作数。
comment:注释。

以一个class类执行Java -c编译看看其方法指令集,以下面这个简单的循环打印方法说明:

public class Test {

    public void print(){
        for (int i=0;i<100;i++){
            System.out.println(i);
        }
    }
}

执行javap -c Test.class的编译结果为

192:vvv$ javap -c Test.class
Compiled from "Test.java"
public class com.test.springbootdemo.Test {
  public com.test.springbootdemo.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void print();
    Code:
       0: iconst_0
       1: istore_1
       2: iload_1
       3: bipush        100
       5: if_icmpge     21
       8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      11: iload_1
      12: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      15: iinc          1, 1
      18: goto          2
      21: return
}

重点关注print方法这段字节码指令,看看其中执行的流程。方法的功能仅仅是for循环打印数字。

索引 指令 含义
0 iconst_0 将常量0压入操作数栈中
1 istore_1 将变量i保存到局部变量表中,其下标为1,此时i=0
2 iload_1 将局部变量表中下标1的变量取出,并压入操作数栈
3 bipush 100 将数值100压入操作数栈,bipush这个指令与iconst效果一样,但会为入栈操作保存直接操作数,因此编译后的结果比iconst会额外多一个字节
5 if_icmpge 21 比较操作变量i是否大于100,大于的话直接跳到21步,推出方法
8 getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11 iload_1 将局部变量表中下表1的变量取出,压入操作数栈
12 invokevirtual #3 / Method java/io/PrintStream.println:(I)V 方法调用
15 iinc 1, 1 参数i执行+1操作
18 goto 2 回到第二步
21 return 推出方法

其实Java虚拟机对class文件进行编译为虚拟机字节码,虚拟机内部是会自动执行优化操作,比如刚才的bipush和iconst两指令,bipush指令与iconst效果一样(都是讲操作数压入操作数栈),但bipush会为入栈操作保存直接操作数,因此编译后的结果比iconst会额外多一个字节。编译器在不需要保存直接操作数时,优先选择使用iconst指令。

2、算术运算举例

示例方法方法:

public int add(int i, int j){
        return (i + j -1) &~ (j-1);
    }

编译后指令码:

public int add(int, int);
    Code:
       0: iload_1
       1: iload_2
       2: iadd
       3: iconst_1
       4: isub
       5: iload_2
       6: iconst_1
       7: isub
       8: iconst_m1
       9: ixor
      10: iand
      11: ireturn

过程拆解如下:

索引 指令 含义
0 iload_1 将局部变量表中下标1参数i压入操作数栈
1 iload_2 将局部变量表中下标2参数j压入操作数栈
2 iadd 从操作数栈中取出i和j,执行i+j,并将结果压入操作数栈
3 iconst_1 将常量1压入操作数栈
4 isub 从操作数栈中取出求和结果和1,执行减法运算,并将结果压入操作数栈,第一个括弧的运算结果
5 iload_2 将局部变量表中下标2参数j压入操作数栈
6 iconst_1 将常量1压入操作数栈
7 isub 从操作数栈中取出j和1,执行减法运算,并将结果压入操作数栈
8 iconst_m1 将常量-1压入操作数栈
9 ixor 执行异或运算,~ (j-1)操作相当于 -1 ^ (j-1),^ 就是异或符号
10 iand 执行与运算,第二个括弧的结果
11 ireturn 返回int类型数值

备注说明,iadd、isub、ixor、iand运算的参数均时从操作数栈中取出。

3、控制结构举例

示例方法:

public void whileDouble(){
        double i=0.0;
        while (i<100.0) {
            i++;
        }
    }

编译后指令码:

public void whileDouble();
    Code:
       0: dconst_0
       1: dstore_1
       2: dload_1
       3: ldc2_w        #4                  // double 100.0d
       6: dcmpg
       7: ifge          17
      10: dload_1
      11: dconst_1
      12: dadd
      13: dstore_1
      14: goto          2
      17: return

这一部分就不进行详细的拆解分析,仅关注其中几个生疏的指令:

索引 指令 含义
3 ldc2_w #4 用于访问类型为double、float的运行时常量池项,double 100.0d
6 dcmpg double类型比较指令,double compare greater,这个指令包含两个步骤:1、push 1 if i is NaN or I >100.0; 2、push 0 if I==100.0;3、push -1 if I <100.0
7 ifge 17 比较指令,if greater than, 转入分支17推出循环。

备注:ldc和ldc_w指令用于访问运行时常量池,包括类String的实例,但不包括double、float类型的值。当常量池类目过多时,会启用ldc_w指令,用于访问类型为double、float的类型值。

dcmpg指令比较场景:

比较 入栈数
大于 1
等于 0
小于 -1

虚拟机常见指令也不少,主要就是先混个眼熟,在偶尔碰到字节码指令时,能够理解即可。

4、异常抛出举例

public void throwException(int i) throws IOException {
        if (i == 0) {
            throw new IOException();
        }
    }

编译后的代码:

public void throwException(int) throws java.io.IOException;
    Code:
       0: iload_1
       1: ifne          12
       4: new           #6                  // class java/io/IOException
       7: dup
       8: invokespecial #7                  // Method java/io/IOException."<init>":()V
      11: athrow
      12: return

过程拆解如下:

索引 指令 含义
0 iload_1 将局部变量表中下标1参数i压入操作数栈
1 ifne 12 if I==0 ,进入if函数体中,分配实例,抛出异常,跳至12步
4 new #6 创建IOException实例
7 dup 复制一个IOException引用
8 invokespecial #7 调用IOException的初始化方法
11 athrow 抛出异常
12 return 仅仅在抛出异常时返回

try…catch结构

public void printOnce(){
        System.out.println("hello");
    }

    public void tryCatchException(){
        try {
            printOnce();
        } catch (Exception e){
            e.printStackTrace();
        }
    }

编译后的代码:

public void printOnce();
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #8                  // String hello
       5: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public void tryCatchException();
    Code:
       0: aload_0
       1: invokevirtual #10                 // Method printOnce:()V
       4: goto          12
       7: astore_1
       8: aload_1
       9: invokevirtual #12                 // Method java/lang/Exception.printStackTrace:()V
      12: return
   	Exception table:
       from    to  target type
           0     4     7   Class java/lang/Exception

过程拆解:

索引 指令 含义
0 aload_0 将this压入操作数栈
1 invokevirtual #10 调用方法,去运行时常量池中索引10的位置找到方法实例的引用 // Method printOnce:()V
4 goto 12 没有异常,直接跳至12步,return方法结束
7 astore_1 将异常对象参数存储到局部变量表中
8 aload_1 将异常对象压入操作数栈
9 invokevirtual #12 调用方法 ,去运行时常量池中索引12的位置找到方法实例的引用 // Method java/lang/Exception.printStackTrace:()V
12 return 返回

异常捕获:针对步骤0-4步中捕获的异常,跳至第7步,即catch语句中

5、片尾曲

编译器场景绝对不是这篇文章能够举例的,本文仅仅将部分场景做了举例,加上作者掌握有限,难免有疏漏之处,如遇错误,请指出。

目的还是在于混个眼熟,遇到时能读懂指令即可。

6、参考资料

1、javap命令oracle官方说明
2、jvm命令说明

你可能感兴趣的:(JAVA,JDK)