- 简单的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; }
|
基于寄存器的执行
- a 加载到 R0寄存器.
- b 加载到 R!寄存器.
- 执行add指令, 结果保存到R2寄存器.
基于栈的执行
- a 加载到栈顶
- b 加载到栈顶
- a,b出栈, 并相加
- 结果入栈.
区别
- 基于栈的方式移植性比较好, 并且比较简单, 毕竟只用维护一个栈就可以了.
- 基于寄存器的不用频繁的出入栈, 但是要维护很多寄存器, 实现更复杂些, 但是执行速度更快了.
栈的具体实现
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++; 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可以看出来过程如下:
- 加载局部变量表0位置的值到操作数栈
- 局部变量表0位置的值加一
- 操作数栈顶元素放回局部变量表0位置, 0覆盖了1
- 所以无论循环多少次, 结果局部变量表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
|
- 局部变量表0位置的值加一
- 加载局部变量表0位置的值到操作数栈
- 操作数栈顶元素放回局部变量表0位置, 等于把局部变量表0位置的值移走又移动回来.
- 所以局部变量能够根据循环数而增加.
如果更复杂点
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
|
- 0 记录到局部变量i上.
- 局部变量i 增加到1;
- 写到操作数栈顶[1]
- 再写到操作数栈顶[1,1]
- 局部变量i 增加到2;
- 栈顶两个元素取出相加, 结果放入栈顶 [2]
- 写到操作数栈顶[2,2]
- 局部变量i 增加到3;
- 栈顶两个元素取出相加, 结果放入栈顶 [4]
- 写到操作数栈顶[3,4]
- 局部变量i 增加到4;
- 栈顶两个元素取出相加, 结果放入栈顶 [7]
- 栈顶元素取出, 放到局部变量i, 用7覆盖了4.
- 最后结果为7.
结论
javap -c -v -l -s -p XXX
是个好命令.