运行时栈帧结构
执行引擎是JVM的核心组件之一。
所有Java虚拟机的执行引擎输入输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。
JVM以方法作为作基本的执行单元。“栈帧”是用于支持虚拟机进行方法调用和方法执行背后的数据结构。这里的方法调用指的是确定调用那一个方法,也是JVM运行时数据区中栈的栈元素。栈帧存储的内容主要有:局部变量表,操作数栈,动态连接和方法返回地址信息等。
局部变量表
局部变量表是一组变量的存储空间。用以存储方法参数和方法内的局部变量。在编译时其最大容量就被确定下来,并被写到方法的Code属性中的max_locals数据项中。
局部便量表的容量以变量槽为单位。在《JVM规范》中并没有明确的指定一个槽占用的内存的大小为多少,而是导向性的说:每个变量槽都应该能存放一个char,boolean,short,int,byte,float,reference,returnAddress
数据类型。这八个数据类型都可以用32位或者更少的物理内存来存储。这意味着一个变量槽所占用的物理内存可以随着处理器、操作系统或者虚拟机实现的不同而发生变化,只是变量槽的外观与32位虚拟机一致。
下面的讨论如果涉及到变量槽没有特殊说明那么都是基于32位虚拟机
reference类型一般需要满足两个条件:
1. 通过该引用能够直接或间接的查找到对象在Java堆中的数据存放的起始地质或索引。
2. 通过该引用能够直接或间接地查找到该对象对应的数据类型在方法区中的存储的类型信息。
returnAddress目前很少见了,用来为字节码指令jsr,jsr_w,ret
服务的,指向一条字节码指令的地址,在一些古老的虚拟机中用来实现异常处理时的跳转,目前都是用异常表来处理异常的。
对于long,double
这两种64位的数据类型,需要两个变量槽来表示。由于局部变量表是建立在线程内,且对其他线程是不可见的,所以不会存在数据竞争和线程安全问题。对于相邻的共同存放一个64位数据结构的变量,虚拟机不允许采取任何方式独立访问其中一个变量槽。若遇到就抛出异常。
当一个方法调用时,若方法不是静态方法,那边变量槽第一个槽中会存放所属对象的引用,也就是this
。变量槽是可以被复用的,当一个变量槽中的变量在字节码执行超过其范围后,就可以被复用。
操作数栈
操作数栈也被称为操作栈,是一个先入后出的栈。Java虚拟机的解释执行引擎被称为”基于栈的执行引擎“,这里的栈指的就是操作数栈。操作数栈的最大深度在Javac编译器编译时便确定了,并写入到Code属性的max_stacks数据项。当操作数栈的元素是long或double
的时候,那么一个数据占用两个栈容量。Javac编译器的数据流分析工作保证了方法在执行的时候,任何是首操作数栈的深度都不会超过max_stacks的值。操作数栈中的数据类型必须与字节码指令的序列严格匹配。
在大多数的虚拟机的实现中,会对栈帧作出一些优化:让下面栈帧的部分操作数栈与上面栈帧的局部变量部分重叠在一起,以节约空间并且在进行方法调用时就可以直接共用一部分数据,无须额外的参数复制传递。
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用,就是为了支持方法调用过程中的动态连接。
动态连接是指Calss文件常量池中的不在类加载阶段和不在第一次使用就转换为直接引用的符号引用,而是在每一次运行期间都转换为直接引用。
方法返回地址
方法在退出的时候,需要返回到最初被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它上层主调方法的执行状态。一般来说法方法正常推出时,主调方法的PC计数器的值可以作为方法返回地址。而方法异常退出时,返回地址是通过异常处理表来确定的,栈帧中不会保存这部分信息。
关于退出,即推出方法的方式,可以分为“正常调用完成”和”异常调用完成“。正常调用完成指的时执行引擎遇到一个方法返回的字节码指令,退出方法。这里的方法字节码返回指令决定了方法返回是否有返回值传递给上层的方法调用者。异常调用完成指的是方法在执行过程中遇到异常(JVM内部产生的、代码中athrow指令禅寺的),只要在本方法的异常表中没有匹配的异常处理器,就会导致方法退出。
方法的退出等同于当前栈帧的出栈,所以可能存在的操作有:1.恢复上层方法的局部变量表和操作数栈 2若有返回值,将返回值压入到调用者栈帧的操作数栈中 3调整PC计数器的值以指向方法调用指令后面的一条指令等。
附加信息
一些《JVM规范》中没有描述的信息,例如与调试、性能收集有关的信息。
—
ubuntu系统弄得,不知道为什么格式不对