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文件结构如下:
通过使用命令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 .<init>...()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对应的汇编语言为:
所有指令的格式如下:
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 //调用<init>方法完成对象的初始化
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虚拟机的学习道路上会向前迈进一大步。。
愿大家共同进步!!