JVM局部变量表和操作数栈的内存布局
局部变量表和操作数栈
首先看一段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=30: bipush 1002: istore_33: iload_14: iload_25: iadd6: iload_37: iadd8: istore 410: iload 412: ireturnLineNumberTable:line 3: 0line 4: 3line 5: 10public static void main(java.lang.String[]);Code:Stack=3, Locals=2, Args_size=10: new #2; //class Add_Sample3: dup4: invokespecial #3; //Method "<init>":()V7: bipush 109: bipush 2011: invokevirtual #4; //Method add:(II)I14: istore_115: getstatic #5; //Field java/lang/System.out:Ljava/io/PrintStream;18: iload_119: invokevirtual #6; //Method java/io/PrintStream.println:(I)V22: 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个元素分别填入了函数参数。
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
的实现
locals_index(rbx);//将字节码指令中的index值放入rbx
__ movl(iaddress(rbx), rax);//将本地变量表中的index值放入rax。为了加快速度,在x86中默认将rax寄存器作为栈顶
如果要执行压栈操作,只需要将sp
减去对应的值并将值放入sp
所在内存即可。这里不举例了,因为在openjdk
的解释器实现时并不是简单的往内存压栈,而是结合了栈顶缓存实现。
总结
局部变量表(locals)包括了函数内部的临时变量和形参,另外还包括了当前函数的this
指针
操作数栈实现了Java虚拟机的栈式操作
在openjdk
的实现中,局部变量表位于解释栈的高地址处,用locals
寄存器定位局部变量表并根据偏移值获取值;操作数栈位于解释栈的低地址,用sp
寄存器定位实现字节码逻辑。