Hotspot Internals in Action(8):处理器、指令与栈之3
5、Java中的栈
与C/C++栈不同,Java栈是由虚拟机自动管理的,对程序员不可见,即程序员不能直接设置栈。Java开发中,每当我们在程序中使用new生成一个对象,对象的引用存放在栈里,而对象是存放在堆里的。可以看出栈在Java核心的重要位置。今天我们就继续深入Java核心这个系列,为您介绍Java中的栈、局部变量及其之间的关系。
每当启用一个线程时,JVM就为他分配一个Java栈,栈是以帧为单位保存当前线程的运行状态。某个线程正在执行的方法称为当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池。当线程执行一个方法时,它会跟踪当前常量池。
每当线程调用一个Java方法时,JVM就会在该线程对应的栈中压入一个帧,这个帧自然就成了当前帧。当执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等等。
Java栈上的所有数据都是私有的。任何线程都不能访问另一个线程的栈数据。所以我们不用考虑多线程情况下栈数据访问同步的情况。
像方法区和堆一样,Java栈和帧在内存中也不必是连续的,帧可以分布在连续的栈里,也可以分布在堆里。
Java栈的组成元素:栈帧
栈帧(stack frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。 每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。 对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
局部变量区和操作数栈的大小要视对应的方法而定,他们是按字长计算的。但调用一个方法时,它从类型信息中得到此方法局部变量区和操作数栈大小,并据此分配栈内存,然后压入Java栈。
局部变量区
局部变量区被组织为以一个字长为单位、从0开始计数的数组,类型为short、byte和char的值在存入数组前要被转换成int值,而long和double在数组中占据连续的两项,在访问局部变量中的long或double时,只需取出连续两项的第一项的索引值即可,如某个long值在局部变量区中占据的索引时3、4项,取值时,指令只需取索引为3的long值即可。
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)。reference类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。 returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过this访问。Slot是可以重用的,当Slot中的变量超出了作用域,那么下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。
操作数栈
Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈。操作数栈也常被称为操作栈。操作数栈和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。可把操作数栈理解为存储计算时临时数据的存储区域。
虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。除了PC寄存器之外,JVM并没有实现任何寄存器。
Java虚拟机的指令是从操作数栈中而不是从寄存器中取得操作数的,因此它的运作方式是基于栈的,而不是基于寄存器的。虚拟机把操作数栈作为它的工作区。
下面我们通过一段简短的程序片段外加一幅图片来了解下操作数栈的作用。
从图中可以得出:操作数栈其实就是个临时数据存储区域,它是通过入栈和出栈来进行操作的。
帧数据区除了局部变量区和操作数栈外,Java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些数据都保存在Java栈帧的帧数据区中。
当JVM执行到需要常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。
除了处理常量池解析外,帧里的数据还要处理Java方法的正常结束和异常终止。如果是通过return正常结束,则当前栈帧从Java栈中弹出,恢复发起调用的方法的栈。如果方法又返回值,JVM会把返回值压入到发起调用方法的操作数栈。
为了处理Java方法中的异常情况,帧数据区还必须保存一个对此方法异常引用表的引用。当异常抛出时,JVM给catch块中的代码。如果没发现,方法立即终止,然后JVM用帧区数据的信息恢复发起调用的方法的帧。然后再发起调用方法的上下文重新抛出同样的异常。
栈的整个结构
在前面就描述过:栈是由栈帧组成,每当线程调用一个Java方法时,JVM就会在该线程对应的栈中压入一个帧,而帧是由局部变量区、操作数栈和帧数据区组成。那在一个代码块中,栈到底是什么形式呢?下面是我从《深入JVM》中摘抄的一个例子,大家可以看看:代码片段:
执行过程中的三个快照:
上面所给的图,只想说明两件事情,我们也可用此来理解Java中的栈:
1、只有在调用一个方法时,才为当前栈分配一个帧,然后将该帧压入栈。
2、帧中存储了对应方法的局部数据,方法执行完,对应的帧则从栈中弹出,并把返回结果存储在调用方法的帧的操作数栈中。
关于java栈的实现,请阅读下一章内容。