JVM 虚拟机栈详解

栈帧

栈帧 (Stack Frame) 是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈 (Virtual  Machine  Stack)的栈元素 帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每方能从调用开始到执行完成的过程,就对应着个栈帧在虚拟机栈里面从入到出的过程

在编译程序代码的时候,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且入到方法表的Code 属性之中 因此一个栈帧需要分配多少内存  不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现

个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于 执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的 称为当前栈帧 (Current Stack Frame) ,这个栈帧所关联的方法称为当前方法 (Current  Method) 执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。

局部变量表

局部变最表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在 Java程序被编译为 Class文件时,就在方法的 Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。

局部变量表的容量以变量槽(variable slot,下称slot)为最小单位,虚拟机规范中并没有明确指明一个slot应占用的内存空间大小,只是很有“导向性”地说明每个slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这种描述与明确指出“每个slot占用32位长度的内存空间”有一些差别,它允许slot的长度随着处理器、操作系统或虚拟机的不同而发生变化。不过无论如何,即使在64位虚拟机中使用了64位长度的内存空间来实现一个slot,虚拟机仍要使用对齐和补白的手段让slot在外观上看起来与32位虚拟机中的一致。

既然前面提到了数据类型,在此顺便说一下,一个slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、refcrcnce和retumAddress八种类型.前面六种不需要多加解释,大家都认识,而后面的referencc是对象的引用。虚拟机规范既没有说明它的长度,也没有明确指出这个引用应有怎样的结构,但是一般来说,虚拟机实现至少都应当能从此引用中直接或间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。而:etumAddress是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。

对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的slot空间。Java语言中明确规定的64位的数据类型只有long和double两种(reference类型则可能是32位也可能是64位)。值得一提的是,这里把long和double数据类型分割存储的做法与“long和double的非原子性协定”中把一次long和double数据类型读写分割为两次32位读写的做法类似,读者阅读到Java内存模型时可以对比一下。不过,由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的 slot 是否是原子操作,都不会引起数据安全问题。

虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的slot数量。如果是32位数据类型的变量,索引n就代表了使用第n个slot, 如果是64位数据类型的变量,则说明要使用第n和第n+l两个slot。在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非 static的方法),那么局部变量表中第0位索引的slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含的参数。其余参数则按照参数表的顺序来排列,占用从1开始的局部变量slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的slot。

局部变量表中的slot是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那么这个变量对应的slot就可以交给其他变量使用。这样的设计不仅仅是为了节省栈空间,在某些情况下slot的复用会直接影响到系统的垃圾收集行为。

局部变量不像成员变量在定义的时候赋初始值。

操作数栈

操作数栈也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO) 栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为l,64位数据类型所占的栈容量为2在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。

另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的。但是大多数虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重登。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,而无须进行额外的参数复制传递了。

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存有大最的符号引用,字节码中的方法调用指令就以常最池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

返回地址

当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。

另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion) 。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。

无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

 

 

 

参考《深入理解Java虚拟机:JVM高级特性与最佳实践》第8章

补充:http://www.it610.com/article/3806641.htm

猜你喜欢

转载自blog.csdn.net/Anenan/article/details/83899970