JVM 2-虚拟机栈

506 阅读6分钟

JVM运行时数据区

虚拟机栈

与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同(线程结束,栈回收,不存在垃圾回收问题)。

虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

编译程序代码时,局部变量表大小、操作数栈深度已经确定;

在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。

在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。

当前栈帧

下面将介绍栈帧的内部结构

1 局部变量

用于存放方法参数方法内部定义的局部变量

局部变量表主要存放了编译器可知的各种数据类型(booleanbytecharshortintfloatlongdouble)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

reference要符合以下两点要求:

  • 从此引用直接或间接地查找到对象在Java堆中的数据存放的起始地址索引(找到对象数据)
  • 从此引用直接或间接地查找到对象所属数据类型在方法区中存储地类型信息(找到类型信息)

最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在JVM中,longdouble类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。由于局部变量表是建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否为原子操作,都不会引起数据安全问题。

如果是非静态方法,则在 index[0] 位置上存储的是方法所属对象的实例引用,一个引用变量占 4 个字节,随后存储的是参数和局部变量。

局部变量表

当前字节码PC计数器的值已经超出某个变量的作用域,这个变量对应的Slot可以交给其他变量使用

public void test() {
    {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
    }
    int a = 0;
}

此时,存放placeHolderSlot已经出了作用域;这个Slota使用

局部变量没有准备阶段, 必须显式初始化。

局部变量声明之后,才会放入局部变量表中;所以在声明之前使用,会报错。

类变量有两次赋初始值的过程

  • 一次在准备阶段,赋予系统初始值
  • 另一次在初始化阶段,赋予程序员定义的初始值

2 操作数栈

一个后入先出的栈;32位数据类型栈容量为1,64位数据类型栈容量为2;

方法刚刚执行的时候,这个方法的操作数栈是空的;方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容 如:

  1. 算术运算
  2. 调用其他方法

操作数栈1

操作数栈2

3 动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。

  • 这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。
  • 另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

在静态解析时候,看到变量fatherchild的静态类型都是Father,所以,doSomething方法都解析成Father类里的方法。而child变量的实际类型是Child,应该调用Child类里的doSomething方法,因此方法区里静态解析式不完全正确,要在运行期间栈帧进栈的时候动态连接到真实的类和方法。

动态连接发生在栈帧完全入栈之前,也在局部变量表等形成之前。

Test类最后一条语句child.doSomething为例。执行到该条语句之时,找到doSomething的直接引用,即在方法区里的地址。形成了指向ChilddoSomething方法的动态连接,找到了该方法的入口,也就找到了对应得字节码指令,有了局部变量表和操作数栈的出入栈,然后形成了doSomething这个方法的栈帧。字节码指令执行完后,doSomething方法的栈帧出栈,根据返回地址,返回到test方法继续执行。

栈帧持有的引用是方法的入口,动态连接是找到正确方法的入口,然后才有后来的进栈出栈的执行。

4 方法返回地址

方法返回地址:存储方法要返回的位置(在上层方法中的位置)

方法执行时有两种退出情况:

  • 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURNIRETURNARETURN 等;会有返回值
  • 异常退出,不会给它的上层调用者产生任何返回值

方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态

一般来说

  • 方法正常退出时,调用者(上层方法)的程序计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。(RETURNIRETURNARETURN返回的目标地址)
  • 方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出时可能有的操作:

  • 恢复上层方法的局部变量表和操作数表
  • 返回值压入上层调用栈帧。
  • PC计数器指向方法调用后的下一条指令。

5 异常

Java 虚拟机栈会出现两种异常:

  • StackOverFlowError(栈溢出): 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异常。
  • OutOfMemoryError(内存溢出): 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 异常。