本文关注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指令。
示例方法方法:
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运算的参数均时从操作数栈中取出。
示例方法:
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 |
虚拟机常见指令也不少,主要就是先混个眼熟,在偶尔碰到字节码指令时,能够理解即可。
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语句中。
编译器场景绝对不是这篇文章能够举例的,本文仅仅将部分场景做了举例,加上作者掌握有限,难免有疏漏之处,如遇错误,请指出。
目的还是在于混个眼熟,遇到时能读懂指令即可。
1、javap命令oracle官方说明
2、jvm命令说明