目录
JVM虚拟机栈简介
JVM(Java Virtual Machine)即Java虚拟机的内存模型按照是否被线程共享可分为两部分:
- 可被线程共享: 堆 和 方法区
- 线程私有:虚拟机栈 和 本地方法栈
在JVM中堆代表了数据存储,栈代表了处理逻辑,JVM栈和JVM堆的分离使得堆中的内容可以被多个JVM栈所共享。
今天我们就来谈谈虚拟机栈,虚拟机栈既然是线程私有的,那么每个线程都应当有其自己的虚拟机栈。而每个虚拟机栈都由基本一个个栈帧构成,栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。
![](https://i-blog.csdnimg.cn/blog_migrate/a32c198e24433b2c1426cd8b7fcd1545.png)
局部变量表
局部变量表(Local Variable Table)变量值存储空间,用于存放方法参数以及方法内部定义的局部变量。
当.java文件被编译成.class文件后,就已经确定了该方法所需要分配的局部变量表容量的最大值。在class文件的方法表的code属性的max_local属性指定了其最大值。
变量槽(Variable Slot)
变量槽大小
局部变量表以变量槽作为最小单位,每个变量槽都可以存储32位长度的内存空间。例如boolean(8bit)、byte(8bit)、char(16bit)、short(16bit)、int(32bit)、float(32bit)、reference、returnAddress 。对于64位长度的数据类型--long、double,虚拟机以高位对齐的方式为其分配两个连续的slot空间,那么一次读写时会有两次访问内存的操作。
变量槽的分配
当调用的方法为实例方法也就是非static方法时,局部变量表中的第0位索引的slot默认是用于传递方法所属对象实例的引用,在该方法中可以通过this关键字来访问到。其余的参数则按照参数列表顺序排列,占用从索引1开始的slot,分配完方法参数后,便依次分配方法内部定义的局部变量。
变量槽的复用
为了节省栈帧的空间,局部变量表中的slot是可以复用的。因为在方法内局部变量有其自己的作用域,当执行到离开某个变量的作用域后,这个变量所对应的slot空间就可以交给其他变量使用。虽然这样可以节省空间,但这种被动的采用新覆盖旧的方式来进行清理,有时会影响垃圾回收行为——当执行到离开某个引用类型变量的作用域时,如果没有新的变量覆盖原来的变量空间,则原来的引用变量会一直指向堆内存的空间,那么当进行垃圾回收时,该引用对应的堆内存就不会被回收。
操作数栈
操作数栈顾名思义就是存储指令操作数的栈空间。操作数栈的每一个位置都可以保存一个java虚拟机中定义的任意数据类型的值,这包括long和double。
在class文件的code属性的max_stacks 指定了执行过程中最大的栈深度。Java虚拟机的解释执行引擎被称为基于栈的执行引擎,这里的栈指的便是操作数栈。
当一个方法开始执行时,该方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和取出内容,也就是入栈和出栈的操作。
方法执行过程
当一个方法被调用时,该方法的栈帧入栈;当方法运行结束(遇到返回指令或者出现异常)则该栈帧出栈。我们可以将栈帧理解成一个方法的运行空间。一个栈帧主要由局部变量表和操作数栈构成。局部变量表存储的是方法的参数以及该方法中定义的局部变量,操作数栈中存放的是操作数,当执行某条带有n个操作数的指令时,就从栈顶去除n个操作数,然后把结果(如果该指令有结果的话)入栈。所以,当我们说JVM的执行引擎是基于栈的时候,这个栈指的是操作数栈,Java字节码指令的操作数存放在操作数栈中,而操作数栈是内存中的一块空间,所以字节码指令不必担心不同机器上寄存器以及机器指令的差别,从而做到了平台无关性。值得一说的是局部变量表以及操作数栈的容量的最大值在编译时就已经确定了,并且在运行时不会改变。
现在我们通过实际的例子来看看方法执行时局部变量表和操作数栈是怎样起作用的。
例如我们有一个方法f:
void f(){
int a = 1+2;
int b = 5+a;
}
那么它对应的字节码以及执行过程为:
iconst_1 //把整数1压入操作数栈
iconst_2 //把整数2压入操作数栈
iadd //将栈顶的两个数(1和2)出栈,相加(1+2),然后结果(3)入栈
//实际上前三步会被编译器优化为iconst_3
istore_1 //把栈顶元素的内容放入局部变量表中索引为1的slot中,也就是a对应的空间。
iconst_5 //把整数5压入操作数栈
iload_1 //把局部变量表中索引为1的slot中存放的变量值(3)压入操作数栈
iadd //将栈顶两个元素(3和5)出栈后相加,然后结果(8)入栈
istore_2 //将栈顶内容放入局部变量表中索引为2的变量槽中,也就是b对应空间中
return //方法返回,程序计数器指内容改为的栈帧中的返回地址,该方法对应的的栈帧出栈
上述字节码我们可以通过javap -c 命令对class文件反汇编得到:
动态链接
(待更新)
方法返回地址
当一个方法开始执行后,只有两种退出当前方法的方式:
- 遇到返回指令return时。此时会将当前方法的返回值传递给上层方法的调用者。调用者的程序计数器的值作为返回地址。
- 当执行遇到异常时,并且当前方法中没有相应的异常处理,就会导致方法退出,此时是没有返回值的。返回地址要通过异常处理表来决定。
当方法返回时会进行三个操作:
- 将当前方法的栈帧出栈
- 将返回值压入调用方法的操作数栈中
- 调整PC计数器的值使其指向调用者方法调用指令后面的一条指令