JVM Stacks :线程私有 先看一下官方文档当中的描述:
jvm 栈是描述java方法执行的内存模型,它的生命周期和线程相同,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。每个方法对应一个栈帧。具体类型结构如图:
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量
局部变量表以变量槽(Variable Slot)为最小单位。特点有以下几个:
1.虚拟机没有明确指定slot应该占的大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。一般是32位。
2.它允许Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化,在64位操作系统中采用填充对齐的方法使其一致
3.long、double这种64位的数据会被分配两个连续Slot空间
4.连续空间可以通过较小的索引来访问数据。
我们知道在java中有两种方法,一种是类方法,一种是实例方法。 在类方法调用中,所有参数都从局部变量0开始在连续的局部变量中传递。 在实例方法调用中,局部变量0始终指向的是该实例对象,也就是this。也就是说真实的参数是从局部变量1开始存储的。
操作数栈(Operand Stack)也常称为操作栈,是一个后入先出的栈(LIFO LAST IN FIRST OUT).操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过max_stacks数据项中设定的最大值。
让我们通过两段段代码来理解一下:
1.事先我们得准备一个plugin:jclasslib Bytecode Viewer。然后通过show bytecode with jclasslib 打开文件。
代码 A
public static void main(String[] args) {
int i = 8;
i = i++;
System.out.println(i);
}
//结果为8
代码 B
public static void main(String[] args) {
int i = 8;
i = ++i;
System.out.println(i);
}
//结果为9
A 本地方法表
代码A 点开 方法-code 字节码文件:
可以直接点击指令进入文档对应位置进行阅读。这边讲解几条
(压栈)
栈顶元素出栈,放入局部变量表号位
将局部变量表 n 号位的值 推入操作数栈
根据索引将局部变量表索引位的数增加常量
我们来根据解释一下 代码A:
1.bipush 8 将8压入操作数栈 , **此时栈内值为8**
2.istore_1 8 出栈 ,放入局部变量表1号位。**此时栈空,局部变量表1号位8**
3.iload_1 将局部变量表1号位的数压栈。 **此时局部变量表1号位 8 ,操作数栈8**
4.iinc 1 by 1 将局部变量表1号位 +1 **此时局部变量表1号位 9 ,操作数栈8**
5.istore_1 8 出栈 ,放入局部变量表1号位。**此时栈空,局部变量表1号位8**
7.iload_1 将局部变量表1号位的数压栈。 **此时局部变量表1号位 8 ,操作数栈8**
代码B:
1.bipush 8 将8压入操作数栈 , **此时栈内值为8**
2.istore_1 8 出栈 ,放入局部变量表1号位。**此时栈空,局部变量表1号位8**
3.iinc 1 by 1 将局部变量表1号位 +1 **此时局部变量表1号位 9 ,操作数栈空**
4.iload_1 将局部变量表1号位的数压栈。 **此时局部变量表1号位 9 ,操作数栈9**
5.istore_1 9 出栈 ,放入局部变量表1号位。**此时栈空,局部变量表1号位9**
……
从网上偷张图(https://www.cnblogs.com/caca/p/jvm_stack_frame.html)。
简而言之,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。字节码中的方法调用会用常量池中指向方法的符号引用作为参数,在类加载阶段就转变成直接引用的就是静态解析,在运行期间转换的为动态连接。
JVM提供了五种invoke指令:
invokestatic:调用静态方法。
invokespecial:调用实例构造器<init>方法、私有方法和父类方法。
invokevirtual:(自带多态)调用所有的虚方法。
invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象
invokedynamic:(lambda表达式函数式调用的时候多用到这条) 先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
简而言之,将其他栈帧的返回值推入调用方操作数栈,适当修改pc使得跳过调用的指令。