前文中说到:“虚拟机栈是线程私有的,每建立一个线程,虚拟机就会为这个线程建立一个虚拟机栈,虚拟机栈表示Java方法执行的内存模型,每调用一个方法就会为每一个方法生成一个栈帧(Stack Frame),用来存储局部变量表、操做数栈、动态连接、方法出口等信息。每一个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程。虚拟机栈的生命周期和线程是相同的”。html
其中,虚拟机栈是一个后入先出的栈。栈帧是保存在虚拟机栈中的,栈帧是用来存储数据和存储部分过程结果的数据结构,同时也被用来处理动态连接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。线程运行过程当中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。以下图所示:数据结构
局部变量表是一组局部变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java文件编译为Class文件时,就在方法表的Code属性的max_locals数据项中肯定了该方法须要分配的最大局部变量表的容量。post
操做数栈也常被称为操做栈,它是一个后入先出栈。JVM底层字节码指令集是基于栈类型的,全部的操做码都是对操做数栈上的数据进行操做,对于每个方法的调用,JVM会创建一个操做数栈,以供计算使用。和局部变量同样。操做数栈的最大深度也是编译的时候写入到方法表的code属性的max_stacks数据项中。操做数栈的每个元素能够是任意的Java数据类型,包括long、double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。栈容量的单位为“字宽”。当一个方法刚刚执行的时候,这个方法的操做数栈是空的,在方法执行的过程当中,会有各类字节码指向操做数栈中写入和提取值,也就是入栈与出栈操做。例如,在作算术运算的时候就是经过操做数栈来进行的,又或者调用其它方法的时候是经过操做数栈来行参数传递的。 另外,在概念模型中,两个栈帧做为虚拟机栈的元素,相互之间是彻底独立的,可是大多数虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下栈帧的部分操做数栈与上面栈帧的部分局部变量表重叠在一块儿,这样在进行方法调用返回时就能够共用一部分数据,而无须进行额外的参数复制传递了。优化
每一个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期期间转化为直接引用,这部分称为动态链接。spa
当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(Normal Method Invocation Completion)。另一种退出方式是,在方法执行过程当中遇到了异常,而且这个异常没有在方法体内获得处理,不管是Java虚拟机内部产生的异常,仍是代码中使用throw字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会致使方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。 不管采用何种方式退出,在方法退出以前,都须要返回到方法被调用的位置,程序才能继续执行,方法返回时可能须要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。通常来讲,方法正常退出时,调用者PC计数器的值就能够做为返回地址,栈帧中极可能会保存这个计数器值。而方法异常退出时,返回地址是要经过异常处理器来肯定的,栈帧中通常不会保存这部分信息。 方法退出的过程实际上等同于把当前栈帧出栈,所以退出时可能执行的操做有:恢复上层方法的局部变量表和操做数栈,把返回值(若是有的话)压入调用都栈帧的操做数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。线程
虚拟机栈的栈元素是栈帧,当有一个方法被调用时,表明这个方法的栈帧入栈;当这个方法返回时,其栈帧出栈。所以,虚拟机栈中栈帧的入栈顺序就是方法调用顺序。什么是栈帧呢?栈帧能够理解为一个方法的运行空间。它主要由两部分构成,一部分是局部变量表,方法中定义的局部变量以及方法的参数就存放在这张表中;另外一部分是操做数栈,用来存放操做数。咱们知道,Java 程序编译以后就变成了一条条字节码指令,其形式相似汇编,但和汇编有不一样之处:汇编指令的操做数存放在数据段和寄存器中,可经过存储器或寄存器寻址找到须要的操做数;而 Java 字节码指令的操做数存放在操做数栈中,当执行某条带 n 个操做数的指令时,就从栈顶取 n 个操做数,而后把指令的计算结果(若是有的话)入栈。所以,当咱们说 JVM 执行引擎是基于栈的时候,其中的“栈”指的就是操做数栈。举个简单的例子对比下汇编指令和 Java 字节码指令的执行过程,好比计算 1 + 2
,在汇编指令是这样的:code
mov ax, 1 ;把 1 放入寄存器 ax
add ax, 2 ;用 ax 的内容和 2 相加后存入 ax
而 JVM 的字节码指令是这样的:orm
iconst_1 //把整数 1 压入操做数栈 iconst_2 //把整数 2 压入操做数栈 iadd //栈顶的两个数相加后出栈,结果入栈
因为操做数栈是内存空间,因此字节码指令没必要担忧不一样机器上寄存器以及机器指令的差异,从而作到了平台无关。htm
注意,局部变量表中的变量不可直接使用,如需使用必须经过相关指令将其加载至操做数栈中做为操做数使用。好比有一个方法 void foo()
,其中的代码为:int a = 1 + 2; int b = a + 3;
,编译为字节码指令就是这样的:blog
iconst_3 //把整数 1 压入操做数栈 iconst_2 //把整数 2 压入操做数栈 iadd //栈顶的两个数出栈后相加,结果入栈;实际上前三步会被编译器优化为:iconst_3 istore_1 //把栈顶的内容放入局部变量表中索引为 1 的 slot 中,也就是 a 对应的空间中 iload_1 // 把局部变量表索引为 1 的 slot 中存放的变量值(3)加载至操做数栈 iconst_3 iadd //栈顶的两个数出栈后相加,结果入栈 istore_2 // 把栈顶的内容放入局部变量表中索引为 2 的 slot 中,也就是 b 对应的空间中 return // 方法返回指令,回到调用点
须要说明的是,局部变量表以及操做数栈的容量的最大值在编译时就已经肯定了,运行时不会改变。而且局部变量表的空间是能够复用的,例如,当指令的位置超出了局部变量表中某个变量 a 的做用域时,若是有新的局部变量 b 要被定义,b 就会覆盖 a 在局部变量表的空间。