JVM字节码之原理一

  • 简单的JVM运行原理
  • 基于栈与基于寄存器的实现方式

两种常见的实现方式

JVM是java平台的根基, 主要负责屏蔽各个操作系统的差异, 让开发者写的同一份程序, 能够在各种平台上执行. 虚拟机常见的实现方式有两种: 基于栈(Stack based)与基于寄存器(Register based). 基于栈的虚拟机有Hotspot JVM, .net CLR. 这种实现方式是比较常见的. 基于寄存器的有LuaVM和DalvikVM(安卓虚拟机). 下面来看看两种有什么不一样.

1
2
3
void bar(int a, int b){
int c = a + b;
}

基于寄存器的执行

  1. a 加载到 R0寄存器.
  2. b 加载到 R!寄存器.
  3. 执行add指令, 结果保存到R2寄存器.

基于栈的执行

  1. a 加载到栈顶
  2. b 加载到栈顶
  3. a,b出栈, 并相加
  4. 结果入栈.

区别

  1. 基于栈的方式移植性比较好, 并且比较简单, 毕竟只用维护一个栈就可以了.
  2. 基于寄存器的不用频繁的出入栈, 但是要维护很多寄存器, 实现更复杂些, 但是执行速度更快了.

栈的具体实现

Hotspot JVM是基于栈实现的, 每个线程都有一个虚拟机栈, 存储的叫做栈帧, 每个方法的执行和结束执行对应了入栈和出栈. 每个栈帧里面包含了局部变量表,操作数栈,和指向运行时常量池的引用.

局部变量表

Local Variables 在编译期间就确定了, 编译时javac能够分析出每个方法属于类方法还是对象方法, 方法的参数有多少个, 方法中使用了多少变量, 方法中是否有异常处理逻辑.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Main {

public static void main(String[] args) {

}

public void testa(int a, double b) {
int tmp = a;
double tmp2 = b;
String str = "Hello";
}


public static void test2(int a, double b) {
int tmp = a;
double tmp2 = b;
String str = "Hello";
}

public static void test3() {
int i = 0;
try {
i = 1;
} catch (Exception e){
i = 3;
} finally {
i = 2;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;

public void testa(int, double);
descriptor: (ID)V
flags: ACC_PUBLIC
Code:
stack=2, locals=8, args_size=3
0: iload_1
1: istore 4
3: dload_2
4: dstore 5
6: ldc #2 // String Hello
8: astore 7
10: return
LineNumberTable:
line 14: 0
line 15: 3
line 16: 6
line 17: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/yuda/Main;
0 11 1 a I
0 11 2 b D
3 8 4 tmp I
6 5 5 tmp2 D
10 1 7 str Ljava/lang/String;

public static void test2(int, double);
descriptor: (ID)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=7, args_size=2
0: iload_0
1: istore_3
2: dload_1
3: dstore 4
5: ldc #2 // String Hello
7: astore 6
9: return
LineNumberTable:
line 21: 0
line 22: 2
line 23: 5
line 24: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 a I
0 10 1 b D
2 8 3 tmp I
5 5 4 tmp2 D
9 1 6 str Ljava/lang/String;
public static void test3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: iconst_0
1: istore_0
2: iconst_1
3: istore_0
4: iconst_2
5: istore_0
6: goto 22
9: astore_1
10: iconst_3
11: istore_0
12: iconst_2
13: istore_0
14: goto 22
17: astore_2
18: iconst_2
19: istore_0
20: aload_2
21: athrow
22: return
Exception table:
from to target type
2 4 9 Class java/lang/Exception
2 4 17 any
9 12 17 any
LineNumberTable:
line 27: 0
line 29: 2
line 33: 4
line 34: 6
line 30: 9
line 31: 10
line 33: 12
line 34: 14
line 33: 17
line 34: 20
line 35: 22
LocalVariableTable:
Start Length Slot Name Signature
10 2 1 e Ljava/lang/Exception;
2 21 0 i I
StackMapTable: number_of_entries = 3
frame_type = 255 /* full_frame */
offset_delta = 9
locals = [ int ]
stack = [ class java/lang/Exception ]
frame_type = 71 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 4 /* same */
  • testa() 方法locals=8, args_size=3, 是一个对象方法, 所以有一个this, 然后是参数列表, 最后是内部的局部变量. double类型占用两个位置, 所以一共有8个(8=this+a+b+b+tmp+tmp2+tmp2+str)
  • test2() 方法locals=7, args_size=3, 因为是个类方法(有static), 所以缺了一个this.
  • test3() 方法locals=3, args_size=0, 因为有一个Exception占用了一个位置, 并且有一个未显示的Exception, 用于athrow指令向外抛出异常.

操作数栈

每个栈帧有一个栈, 一些指令的执行需要依赖于这个, 比如1+2加法就行先把加数和被加数压入栈内(iconst_1,iconst_2), 让他们都处于栈顶, 然后iadd指令,取出栈顶2个元素相加, 最后把结果放到栈顶, JVM的其他指令也类似于这种操作进行.

i++++i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {

public static void main(String[] args) {
fun();
}

public static void fun() {
int i = 0;
for (int j = 0; j < 50; j++)
// i = ++i;
i = i++;
System.out.println(i);
}
}

通过字节码看出区别是:

iinc 指令能直接对局部变量表操作

i++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 0: iconst_0
1: istore_0
2: iconst_0
3: istore_1
4: iload_1
5: bipush 50
7: if_icmpge 21

10: iload_0
11: iinc 0, 1
14: istore_0

15: iinc 1, 1
18: goto 4

21: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_0
25: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
28: return

从10-14可以看出来过程如下:

  1. 加载局部变量表0位置的值到操作数栈
  2. 局部变量表0位置的值加一
  3. 操作数栈顶元素放回局部变量表0位置, 0覆盖了1
  4. 所以无论循环多少次, 结果局部变量表0位置都是0.

++i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 0: iconst_0
1: istore_0
2: iconst_0
3: istore_1
4: iload_1
5: bipush 50
7: if_icmpge 21

10: iinc 0, 1
13: iload_0
14: istore_0

15: iinc 1, 1
18: goto 4

21: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_0
25: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
28: return
  1. 局部变量表0位置的值加一
  2. 加载局部变量表0位置的值到操作数栈
  3. 操作数栈顶元素放回局部变量表0位置, 等于把局部变量表0位置的值移走又移动回来.
  4. 所以局部变量能够根据循环数而增加.

如果更复杂点

1
2
3
4
5
public static void fun() {
int i = 0;
i= ++i + i++ + i++ + i++;
System.out.println("i=" + i);
}

结果为7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 0: iconst_0
1: istore_0
2: iinc 0, 1
5: iload_0
6: iload_0
7: iinc 0, 1
10: iadd
11: iload_0
12: iinc 0, 1
15: iadd
16: iload_0
17: iinc 0, 1
20: iadd
21: istore_0
  1. 0 记录到局部变量i上.
  2. 局部变量i 增加到1;
  3. 写到操作数栈顶[1]
  4. 再写到操作数栈顶[1,1]
  5. 局部变量i 增加到2;
  6. 栈顶两个元素取出相加, 结果放入栈顶 [2]
  7. 写到操作数栈顶[2,2]
  8. 局部变量i 增加到3;
  9. 栈顶两个元素取出相加, 结果放入栈顶 [4]
  10. 写到操作数栈顶[3,4]
  11. 局部变量i 增加到4;
  12. 栈顶两个元素取出相加, 结果放入栈顶 [7]
  13. 栈顶元素取出, 放到局部变量i, 用7覆盖了4.
  14. 最后结果为7.

结论

javap -c -v -l -s -p XXX 是个好命令.