001. 字节码运行机制
1. 一个简单的例子
还是之前的例子,java代码为:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, World");
}
}
将它编译之后生成class文件,使用xxd Hello.class命令得到二进制码
00000000: cafe babe 0000 0034 001d 0a00 0600 0f09 .......4........
00000010: 0010 0011 0800 120a 0013 0014 0700 1507 ................
00000020: 0016 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 046d 6169 umberTable...mai
00000050: 6e01 0016 285b 4c6a 6176 612f 6c61 6e67 n...([Ljava/lang
00000060: 2f53 7472 696e 673b 2956 0100 0a53 6f75 /String;)V...Sou
00000070: 7263 6546 696c 6501 000a 4865 6c6c 6f2e rceFile...Hello.
00000080: 6a61 7661 0c00 0700 0807 0017 0c00 1800 java............
00000090: 1901 000c 4865 6c6c 6f2c 2057 6f72 6c64 ....Hello, World
000000a0: 0700 1a0c 001b 001c 0100 0548 656c 6c6f ...........Hello
000000b0: 0100 106a 6176 612f 6c61 6e67 2f4f 626a ...java/lang/Obj
000000c0: 6563 7401 0010 6a61 7661 2f6c 616e 672f ect...java/lang/
000000d0: 5379 7374 656d 0100 036f 7574 0100 154c System...out...L
000000e0: 6a61 7661 2f69 6f2f 5072 696e 7453 7472 java/io/PrintStr
000000f0: 6561 6d3b 0100 136a 6176 612f 696f 2f50 eam;...java/io/P
00000100: 7269 6e74 5374 7265 616d 0100 0770 7269 rintStream...pri
00000110: 6e74 6c6e 0100 1528 4c6a 6176 612f 6c61 ntln...(Ljava/la
00000120: 6e67 2f53 7472 696e 673b 2956 0021 0005 ng/String;)V.!..
00000130: 0006 0000 0000 0002 0001 0007 0008 0001 ................
00000140: 0009 0000 001d 0001 0001 0000 0005 2ab7 ..............*.
00000150: 0001 b100 0000 0100 0a00 0000 0600 0100 ................
00000160: 0000 0100 0900 0b00 0c00 0100 0900 0000 ................
00000170: 2500 0200 0100 0000 09b2 0002 1203 b600 %...............
00000180: 04b1 0000 0001 000a 0000 000a 0002 0000 ................
00000190: 0003 0008 0004 0001 000d 0000 0002 000e ................
在使用 javap -c -s Hello指令之后将会得到以下的字节码
Compiled from "Hello.java"
public class Hello extend Object{
public Hello();
descriptor: ()V
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello, World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
2. 基于栈的执行引擎
虚拟机从实现上可以分为两种,以基于栈的实现方式,以基于寄存器的实现方式。
当然两种方式各有优劣,我们目前的JVM是基于栈的实现方式。
我们把目光聚焦到一个简单方法上
public void f(int a,int b){
int c = a + b;
}
首先,一个方法对应了一个叫做帧栈的东西,就可以理解为一个奇怪的数据结构。
这个数据结构呢,包含三个东西
- 指向运行时常量池的引用
- 局部变量表(想象成固定长度的数组)
- 操作数栈(想象成后进先出的堆栈)
看起来可能很抽象,我们打个比方
上面的方法体编译之后就会变成
0: iload_1 // 将 a 压入操作数栈
1: iload_2 // 将 b 压入操作数栈
2: iadd // 将栈顶两个值出栈,相加,然后将结果放回栈顶
3: istore_3 // 将栈顶值存入局部变量表中第 3 个 slot 中
把我们自己身处在这个方法体里面,当执行到iload_1了,那么就把a塞到操作数栈,当执行iload_2就把b塞到操作数栈
当执行iadd时,这个指令本身需要两个参数执行,那么就自然要到操作数栈先后搞两个参数出来,我们称为出栈
拿到参数,送进iadd内之后有返回值了,把这个返回值塞回操作数栈,类似于iadd这种操作符,都会把得到的结果塞回栈顶,之后的istore_3指令,把产生出来的结果保存到3局部变量表的位置上。
局部变量表永远都是预先分配好的,例如int c = a + b;当在编译的时候就会被认为c是个局部变量,就是需要在局部变量表存在着的。
所以字节码运行永远都是局部变量与操作数栈不断load,store的过程
3 执行过程
// Compiled from "Hello.java"
1. public class Hello {
2. public Hello();
3. descriptor: ()V
4. Code:
5. 0: aload_0
6. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
7. 4: return
8.
9. public static void main(java.lang.String[]);
10. descriptor: ([Ljava/lang/String;)V
11. Code:
12. 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
13. 3: ldc #3 // String Hello, World
14. 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15. 8: return
16.}
3 ~ 7 行:可以看到虽然没有写 Hello 类的构造器函数,编译器会自动加上一个默认构造器函数
5 行:aload_0 这个操作码是 aload_x 格式操作码中的一个。它们用来把对象引用加载到操作数栈。 x 表示正在被访问的局部变量数组的位置。在这里的 0 代表什么呢?我们知道非静态的函数都有第一个默认参数,那就是 this,这里的 aload_0 就是把 this 入栈
6 行:invokespecial #1,invokespecial 指令调用实例初始化方法、私有方法、父类方法,#1 指的是常量池中的第一个,这里是方法引用java/lang/Object."":()V,也即构造器函数
7 行:return,这个操作码属于 ireturn、lreturn、freturn、dreturn、areturn 和 return 操作码组中的一员,其中 i 表示 int,返回整数,同类的还有 l 表示 long,f 表示 float,d 表示 double,a 表示 对象引用。没有前缀类型字母的 return 表示返回 void
到此为止,默认构造器函数就讲完了,接下来,我们来看 9 ~ 14 行的 main 函数
12 行:getstatic #2,getstatic 获取指定类的静态域,并将其值压入栈顶,#2 代表常量池中的第 2 个,这里表示的是java/lang/System.out:Ljava/io/PrintStream;,其实就是java.lang.System 类的静态变量 out(类型是 PrintStream)
13 行:ldc #3、,ldc 用来将常量从运行时常量池压栈到操作数栈,#3 代表常量池的第三个(字符串 Hello, World)
14 行:invokevirtual #4,invokevirutal 指令调用一个对象的实例方法,#4 表示 PrintStream.println(String) 函数引用,并把栈顶两个元素出栈
15行:return 返回void