bboyjing's blog

自己动手写JVM十二【运行时数据区(二)】

本章我们来重点分析下局部变量表和操作数栈,并测试本章代码。

局部变量表和操作数栈实例详解

下面以圆形的周长为例进行分析,首先简单的Java代码如下:

1
2
3
4
5
6
7
8
9
10
11
public class Math {
public static void main(String[] args) {
System.out.println(circumference(1.6f));
}
public static float circumference(float r) {
float pi = 3.14f;
float c = 2 * pi * r;
return c;
}
}

编译该类,class文件如下图所示:
jvmgo_29
可以看出circumference()方法的局部变量表max_locals大小是3,操作数栈max_stack深度是2。再使用javap -c对代码进行反汇编来查看运行指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cd ~/IdeaProjects/jvmgo/java
mvn clean compile
cd target/classes/cn/didadu
javap -c Math
输出如下
public class cn.didadu.Math {
...
public static float circumference(float);
Code:
0: ldc #6 // float 3.14f
2: fstore_1
3: fconst_2
4: fload_1
5: fmul
6: fload_0
7: fmul
8: fstore_2
9: fload_2
10: freturn
}

假设调用可以看出circumference()方法时,传递给它的参数时1.6f,方法开始执行前,栈帧的状态如下图:
jvmgo_30
第一条指令是ldc,它把3.14f推入栈顶,此时栈帧状态如下图所示:
jvmgo_31
注意一下,图上把局部变量表和操作数栈过去的状态也列出来了,最下面一条时当前状态。下一条指令是fstore_1,它把栈顶的3.14f弹出,放到#1号局部变量中,栈帧的状态如图所示:
jvmgo_32
第三条指令fconst_2把2.0f推到栈顶,栈帧的状态如下图所示:
jvmgo_33
第四条指令fload_1把#1号局部变量推入栈顶(更确切地说应该是复制),栈帧的状态如下图所示:
jvmgo_34
由于fload_1指令是把3.14f复制到栈顶,所以3.14f在局部变量表中。下一条指令是fmul,它把栈顶的两个浮点数弹出、相乘,然后再把结果推入栈顶,栈帧的状态如下图所示:
jvmgo_35
第六条指令fload_0指令把#0号局部变量推入栈顶,栈帧的状态如下图所示:
jvmgo_36
第七条指令fmul,继续把栈顶的两个浮点数弹出、相乘,然后再把结果推入栈顶,栈帧的状态如下图所示:
jvmgo_37
第八条指令fload_2把操作数栈顶的float值弹出,放入#2号局部变量表,栈帧的状态如下图所示:
jvmgo_38
第九条指令fload_2把#2号局部变量推入操作数栈顶,栈帧的状态如下图所示:
jvmgo_39
最后freturn指令把操作数栈顶的float变量弹出,返回给方法调用者,,栈帧的状态如下图所示:
jvmgo_40

我们大致理了一下circumference()方法的运行过程,后面会学到更多的Java虚拟机指令集。不过,上述第八条和第九条指令集好像做了无用功,下面我们尝试调整下circumference()方法:

1
2
3
4
5
public static float circumference(float r) {
float pi = 3.14f;
//float c = 2 * pi * r;
return 2 * pi * r;
}

此时再看运行指令:

1
2
3
4
5
6
7
8
9
10
public static float circumference(float);
Code:
0: ldc #6 // float 3.14f
2: fstore_1
3: fconst_2
4: fload_1
5: fmul
6: fload_0
7: fmul
8: freturn

可以看出少了原来的第八条、第九条指令,不管从哪个角度看,少两条指令总归是好的吧,看来以后写代码能注意就注意。

测试

本节简单测试局部变量表和操作数栈,修改main.go文件:

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
// 测试局部变量表
func testLocalVars(vars rtdata.LocalVars) {
vars.SetInt(0, 100)
vars.SetInt(1, -100)
vars.SetLong(2, 2997924580)
vars.SetLong(4, -2997924580)
...
println(vars.GetInt(0))
println(vars.GetInt(1))
println(vars.GetLong(2))
println(vars.GetLong(4))
...
}
// 测试操作数栈
func testOperandStack(ops *rtdata.OperandStack) {
ops.PushInt(100)
ops.PushInt(-100)
ops.PushLong(2997924580)
ops.PushLong(-2997924580)
...
println(ops.PopLong())
println(ops.PopLong())
println(ops.PopInt())
println(ops.PopInt())
...
}
// 模拟启动JVM
func startJVM(cmd *cmd.Cmd) {
frame := rtdata.NewFrame(100, 100)
testLocalVars(frame.LocalVars())
testOperandStack(frame.OperandStack())
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cd /home/zhangjing/IdeaProjects/jvmgo/go
go install cn.didadu/jvmgo
./bin/jvmgo String
#输出如下:
100
-100
2997924580
-2997924580
...
0x0
0x0
+2.718282e+000
+3.141593e+000
...

运行时数据区的学习暂且就到这里,我们初步实现了Tread、Stack、Frame、OperandStack和LocalVars等线程私有的运行时数据区。