局部变量表和操作数栈
首先看一段Java源码
java
public class Add_Sample{
public int add(int i, int j){
int k = 100;
int result = i + j + k;
return result;
}
public static void main(String[] args){
int result = new Add_Sample().add(10,20);
System.out.println(result);
}
}
使用javac Add_Sample.java
进行编译
使用javap -v Add_Sample
查看生成的相关函数的字节码
public int add(int, int);
Code:
Stack=2, Locals=5, Args_size=3
0: bipush 100
2: istore_3
3: iload_1
4: iload_2
5: iadd
6: iload_3
7: iadd
8: istore 4
10: iload 4
12: ireturn
LineNumberTable:
line 3: 0
line 4: 3
line 5: 10
public static void main(java.lang.String[]);
Code:
Stack=3, Locals=2, Args_size=1
0: new #2; //class Add_Sample
3: dup
4: invokespecial #3; //Method "<init>":()V
7: bipush 10
9: bipush 20
11: invokevirtual #4; //Method add:(II)I
14: istore_1
15: getstatic #5; //Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_1
19: invokevirtual #6; //Method java/io/PrintStream.println:(I)V
22: return
如何查看局部变量表和操作数栈的深度
这里可以查看add函数和main函数的Code下面的值,以add函数为例
public int add(int, int);
Code:
Stack=2, Locals=5, Args_size=3
这里可以看到Stack=2
,表示操作数栈的深度是2
Locals=5
,表示局部变量表的个数是5
Args_size=3
,表示参数个数为3。这里相比于形参多了一个参数,这个参数就是该函数的this
指针。

局部变量表和操作数栈如何配合完成计算
回到add函数的字节码,main函数将10和20作为参数传递给add函数。所以本地变量表的前3个元素分别填入了函数参数。
assembly
0: bipush 100
2: istore_3
3: iload_1
4: iload_2
5: iadd
6: iload_3
7: iadd
8: istore 4
10: iload 4
上述字节码的执行步骤如下


经过上述步骤就完成了计算过程
局部变量表和操作数栈在内存中的实际布局
在openjdk
中,其实际布局分别位于解释栈的高地址和低地址,用locals寄存器和sp寄存器进行定位。在x64
架构中,rlocals
是r14
,sp
是rsp
。

如果要获得第2个参数,那么只需要执行将locals的地址加上对应的偏移即可获得。
举例istore
字节码在x64
的实现
assembly
locals_index(rbx);//将字节码指令中的index值放入rbx
__ movl(iaddress(rbx), rax);//将本地变量表中的index值放入rax。为了加快速度,在x86中默认将rax寄存器作为栈顶
如果要执行压栈操作,只需要将sp
减去对应的值并将值放入sp
所在内存即可。这里不举例了,因为在openjdk
的解释器实现时并不是简单的往内存压栈,而是结合了栈顶缓存实现。
总结
局部变量表(locals)包括了函数内部的临时变量和形参,另外还包括了当前函数的this
指针
操作数栈实现了Java虚拟机的栈式操作
在openjdk
的实现中,局部变量表位于解释栈的高地址处,用locals
寄存器定位局部变量表并根据偏移值获取值;操作数栈位于解释栈的低地址,用sp
寄存器定位实现字节码逻辑。