Java虚拟机:Java虚拟机编译器

Java虚拟机是为支持Java编程语言而设计的。Oracle的JDK软件包括两部分内容:一部分是将Java源代码编译成Java虚拟机的指令集的编译器,另一部分是用于实现Java虚拟机的运行时环境。术语“编译器”在某些场景下专指把Java虚拟机指令集转换为特定CPU指令集的翻译器。而在本博文中,编译器是指那种把Java语言编写的源代码编译为Java虚拟机指令集的编译器。

有几个常用的JDK命令大家肯定都非常熟悉,假设有个源文件Test.java,文件的内容为:

class Test {
    public static void main(String[] args) {
        int c = sum(10, 11);
        System.out.println("c = " + c);
    }

    public static int sum(int a, int b) {
        return a + b;
    }
}
  • javac:javac命令可以将Test.java源文件编译为Test.class文件,命令格式为:javac Test.java。

Java Class文件结构如下:

Java虚拟机:Java虚拟机编译器_第1张图片

通过使用命令xxd Test.class,输出的Test.class的内容为

00000000: cafe babe 0000 0034 002f 0a00 0c00 170a  .......4./......
00000010: 000b 0018 0900 1900 1a07 001b 0a00 0400  ................
00000020: 1708 001c 0a00 0400 1d0a 0004 001e 0a00  ................
00000030: 0400 1f0a 0020 0021 0700 2207 0023 0100  ..... .!.."..#..
00000040: 063c 696e 6974 3e01 0003 2829 5601 0004  ....()V...
00000050: 436f 6465 0100 0f4c 696e 654e 756d 6265  Code...LineNumbe
00000060: 7254 6162 6c65 0100 046d 6169 6e01 0016  rTable...main...
00000070: 285b 4c6a 6176 612f 6c61 6e67 2f53 7472  ([Ljava/lang/Str
00000080: 696e 673b 2956 0100 0373 756d 0100 0528  ing;)V...sum...(
00000090: 4949 2949 0100 0a53 6f75 7263 6546 696c  II)I...SourceFil
000000a0: 6501 0009 5465 7374 2e6a 6176 610c 000d  e...Test.java...
000000b0: 000e 0c00 1300 1407 0024 0c00 2500 2601  .........$..%.&.
000000c0: 0017 6a61 7661 2f6c 616e 672f 5374 7269  ..java/lang/Stri
000000d0: 6e67 4275 696c 6465 7201 0004 6320 3d20  ngBuilder...c = 
000000e0: 0c00 2700 280c 0027 0029 0c00 2a00 2b07  ..'.(..'.)..*.+.
000000f0: 002c 0c00 2d00 2e01 0004 5465 7374 0100  .,..-.....Test..
00000100: 106a 6176 612f 6c61 6e67 2f4f 626a 6563  .java/lang/Objec
00000110: 7401 0010 6a61 7661 2f6c 616e 672f 5379  t...java/lang/Sy
00000120: 7374 656d 0100 036f 7574 0100 154c 6a61  stem...out...Lja
00000130: 7661 2f69 6f2f 5072 696e 7453 7472 6561  va/io/PrintStrea
00000140: 6d3b 0100 0661 7070 656e 6401 002d 284c  m;...append..-(L
00000150: 6a61 7661 2f6c 616e 672f 5374 7269 6e67  java/lang/String
00000160: 3b29 4c6a 6176 612f 6c61 6e67 2f53 7472  ;)Ljava/lang/Str
00000170: 696e 6742 7569 6c64 6572 3b01 001c 2849  ingBuilder;...(I
00000180: 294c 6a61 7661 2f6c 616e 672f 5374 7269  )Ljava/lang/Stri
00000190: 6e67 4275 696c 6465 723b 0100 0874 6f53  ngBuilder;...toS
000001a0: 7472 696e 6701 0014 2829 4c6a 6176 612f  tring...()Ljava/
000001b0: 6c61 6e67 2f53 7472 696e 673b 0100 136a  lang/String;...j
000001c0: 6176 612f 696f 2f50 7269 6e74 5374 7265  ava/io/PrintStre
000001d0: 616d 0100 0770 7269 6e74 6c6e 0100 1528  am...println...(
000001e0: 4c6a 6176 612f 6c61 6e67 2f53 7472 696e  Ljava/lang/Strin
000001f0: 673b 2956 0020 000b 000c 0000 0000 0003  g;)V. ..........
00000200: 0000 000d 000e 0001 000f 0000 001d 0001  ................
00000210: 0001 0000 0005 2ab7 0001 b100 0000 0100  ......*.........
00000220: 1000 0000 0600 0100 0000 0100 0900 1100  ................
00000230: 1200 0100 0f00 0000 4200 0300 0200 0000  ........B.......
00000240: 2210 0a10 0bb8 0002 3cb2 0003 bb00 0459  ".......<......Y
00000250: b700 0512 06b6 0007 1bb6 0008 b600 09b6  ................
00000260: 000a b100 0000 0100 1000 0000 0e00 0300  ................
00000270: 0000 0300 0800 0400 2100 0500 0900 1300  ........!.......
00000280: 1400 0100 0f00 0000 1c00 0200 0200 0000  ................
00000290: 041a 1b60 ac00 0000 0100 1000 0000 0600  ...`............
000002a0: 0100 0000 0800 0100 1500 0000 0200 16    ...............
  • java: 通过此命令可以运行main方法放在所在的类。命令格式为:java Test(后面不带.class)。
  • javap:javap这个命令大家可能用的比较少,我用这个命令主要生成非正式的“虚拟机汇编语言”。命令格式为:javap -c Test.class,通过此命令,Test.class对应的汇编语言为:
    Java虚拟机:Java虚拟机编译器_第2张图片

所有指令的格式如下:
index opcode [operand1 operand2 …] [comment]

其中index是相对于方法起始处的字节偏移量,opcode为指令的操作码的助记符号,operandN是指令的操作数,一条指令可以有0个或者多个操作数,comment为行尾的注释。

通过生成的汇编语言我们能够更清楚地明白每个方法在Java虚拟机中的执行情况(我清楚大家可能对上面的汇编语言很懵逼,没关系,上面的汇编语言大家先不要看,我接下来的例子中会分别给出每条汇编语言的注释。。大家也可以找个汇编语言手册瞄一眼,最多也就是256个操作码)

案例分析

之前没有怎么直接读java中每个类对应的“非正式汇编语言”,刚开始看的时候感觉这是什么鬼,读起来还是挺费劲的。但多看看简短程序的汇编语言,感觉对Java虚拟机内部的执行流程加深了太多,前面的一篇博客中介绍了Java虚拟机的内部结构,接下来咱们就通过分析所写方法所对应的汇编语言,来看看方法的执行过程是怎样基于内部结构的。。

算数运算

定义一个方法sum:

public int sum(int a, int b) {
    return a + b;
}

注:上一篇博文中提到,每进入一个新的方法,都会创建一个新的栈帧,即方法是与栈帧对应的。而栈帧又由三部分组成:局部变量表、操作数栈和当前方法所属类的运行时常量池的引用。

sum方法的编译代码如下:

public int sum(int, int);
Code:
    0: iload_1 //将局部变量表中索引为1中的值取出放入操作数栈
    1: iload_2 //将局部变量表中索引为2中的值取出放入操作数栈
    2: iadd // 弹出操作数栈的前两个元素并相加,将结果放入操作数栈
    3: ireturn // 弹出操作数栈的栈顶元素并返回。(注:使用ireturn,需要保证栈顶元素为整型,否则会抛异常)

访问运行时常量池

很多数值常量,以及对象、字段和方法,都是通过当前类的运行时常量池进行访问的。所需要的指令有:ldc、ldc_w和ldc2_w等。

定义一个方法:

public void useManyNumeric() {
    int i = 100;
    int j = 1000000;
    int l1 = 1;
}

useManyNumeric方法的编译代码如下:

public void useManyNumeric();
Code:
    0: bipush 100 //将常数100放入操作数栈
    2: istore_1 // 弹出操作数栈的栈顶元素并放入局部变量表索引为1的位置上
    3: ldc #6 //#6其实就表示运行时常量池中1000000地址的引用。取出1000000并放入操作数栈中
    5: istore_2 //弹出操作数栈的栈顶元素并放入局部变量表索引为2的位置上
    6: lconst_1 //长整数1进入操作数栈
    7: lstore_3 //弹出栈顶元素并放入局部变量表索引为3的位置上
    8: return //当方法的返回值为void时,调用return指令

方法调用

定义如下方法:

public void a() {
        System.out.println("我是被调用的方法");
    }

    public void b() {
        a();
    }

上述b方法的编译代码如下:

public void b();
Code:
    0: aload_0 // 之前也提到过,如果一个方法是非static方法时,局部变量表索引为0的位置上将存储的是当前方法所属类的实例的引用(this),所以如果想在一个方法中访问另一个非static方法时,首先让实例的引用this进栈。
    1: invokevirtual #8 //通过栈中的this,和给出的a方法所在运行时常量池的地址#8,使用invokevirtual来调用a方法。
    4: return // 方法的返回值为void时,直接使用return指令

使用类实例

Java虚拟机类实例通过Java虚拟机的new指令来创建。

定义如下方法:

Object create() {
    return new Object();
}

create放法的编译代码如下:

java.lang.Object create();
Code:
    0: new #9 //创建一个Object实例,并将实例的引用放入到操作数栈中
    3: dup //将栈顶的元素复制一份同时放到操作数栈中
    4: invokespecial #1 //调用方法完成对象的初始化
    7: areturn // 返回对应的对象引用

数组

在Java虚拟机中,数组也是用对象来表示。数组由专门的指令集来创建和操作。比如:newarray、anewarray、multianewarray。

定义一个方法:

void createBuffer() {
    int[] buffer = new int[100];
    buffer[10] = 30;
}

createBuffer的编译代码如下:

void createBuffer();
Code:
    0: bipush 100 //常数100进栈
    2: newarray int //创建一个整型的数组,并且数组的引用进栈
    4: astore_1 // 弹出栈顶的数组的引用放入到局部变量表索引为1的位置上
    5: aload_1 // 局部变量表索引为1的数组的引用进栈
    6: bipush 10 // 常数10进栈
    8: bipush 30 //常数30进栈
    10: iastore //将常数30赋值给数组的索引为10的位置
    11: return //返回

同步

Java虚拟机中的同步使用monitor的进入和退出来实现的。无论是显式同步(有明确的monitorenter和monitorexit),还是隐式同步(依赖方法调用和返回指令实现)都是如此。同步方法并不是使用monitorenter和monitorexit来实现的,而是由方法调用指令调用运行时常量池的ACC_SYNCHRONIZED标志来隐式实现的。

定义一个方法:

void onlyMe(Objet o) {
    Synchronized(o) {
        a();
    }
}

onlyMe方法的编译代码如下:

void onlyMe(java.lang.Object);
Code:
    0: aload_1 // 参数o进栈
    1: dup //复制一份栈顶元素进栈
    2: astore_2 //弹出栈顶元素并存入局部变量表索引为2的位置上
    3: monitorenter // 进入与对象o关联的monitor
    4: aload_0 // 当前方法所属的实例的引用进栈
    5: invokevirtual #8 //调用a方法
    8: aload_2 // 局部变量表索引为2的对象o进栈
    9: monitorexit //退出与对象o关联的monitor
    10: goto 18 // 跳转到18
    13: astore_3 // 如果程序出现异常,则栈顶元素会是一个异常对象,将栈顶元素弹出存放到局部变量表的索引为3的位置上
    14: aload_2 //对象o进栈
    15: monitorexit //退出关于对象o的monitor
    16: aload_3 //异常对象进栈
    17: athrow //返回异常对象给方法的调用者
    18: return //返回
Exception table:
    from to target type
    4     10  13    any
    13    16  13    any


总结:大家可以根据上面所写的内容,自己在本地测试时,编译出对应的汇编语言。我相信,通过汇编语言的理解过程,咱们在Java虚拟机的学习道路上会向前迈进一大步。。

愿大家共同进步!!

你可能感兴趣的:(Java虚拟机)