每个栈帧中存储着:
局部变量表(Local Variables)
操作数栈(Ooperand Stack)(或表达式栈)
动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
方法返回地址(Return Address)(或方法正常退出或异常退出的定义)
一些附加信息
局部变量表:
局部变量表也被称之为局部变量数组或本地变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型、对象引用以及returnAddress类型
由于局部变量表示建立在线程的栈上,是线程私有数据,因此不存在数据安全问题
局部变量表所需的容量大小是在编译期确定下来的,并保存Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表大小的。
局部变量表,最基本的存储单元是Slot(变量槽, 32位的类型占用一个slot,64位的类型占两个槽)
JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量中的每一个slot上
如果需要访问局部变量表中的一个64bit的局部变量值时,只需要使用前一个索引即可
如果当前帧是有构造方法说着实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数顺序表顺序继续排列。
操作数栈**:
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的的操作数栈,也可以称之为**表达式栈
操作数栈,在方法执行过程中,根据字节码指令,忘栈中写入数据或提取数据,即入栈出栈
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中的变量临时的存储空间
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值
栈中的任意一个元素都可以是任意的java数据类型
32bit用一个栈单位深度
64bit用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
栈顶缓存技术
由于操作数是存储在内存中的,因此频繁的执行内存读写操作会影响执行速度,为了解决这个问题,HotSpot JVM 的设计者提出来栈顶缓存技术,将栈顶元素全部缓存到物理cpu的寄存器中,以此降低对内存的读写次数,提升执行效率
动态链接
- 部分符号引用在类加载阶段(解析)的时候就转化为直接引用,这种转化为静态链接
- 部分符号引用在运行期间转化为直接引用,这种转化为动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。
包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接,比如invokedynamic指令
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。
比如: 描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
非虚方法
如果方法在编译期就确定了具体的调用版本,这个版本在运行期间是不可以改变的
这样的方法称为虚方法
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
其他方法称为虚方法
虚拟机提供了以下几条方法调用指令
普通调用指令
invokestatic: 调用静态方法,解析阶段确定唯一方法版本
invokespecial: 调用
invokevirtual: 调用所有虚方法
invokeinterface: 调用接口方法**
动态调用指令:
invokedynamic: 动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可以认为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法
Java语言中方法重写和重载的本质**
- 编译时,哪个类调用哪个重载方法「静态分派过程」。
因为静态分派主要参考「静态类型」,我们可以得出一个范式静态类型.eat(静态类型 o)
- 运行期间,到底执行哪个版本的方法,属于「动态分派过程」。
动态分派根据运行期实际类型查找,son 调用时,先找解析的方法名称及描述符一样的方法声明,如果没有在从父类 father 找。
方法返回地址
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置,**方法正常退出时,调用者的pc寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址**。而通过异常退出的,返回地址是要通过异常表来确定,一般不会保存这部分信息。
**
五道面试题
1.举例栈溢出的情况(StackOverflowError) (通过Xss设置栈的大小)
2.调整栈大小,就能保证不出现溢出吗 (不能)
3.分配的栈内存越大越好吗 (不是, 比如线程数会减少)
4.垃圾回收是否会涉及到虚拟机栈 (不会)
5.方法中定义的局部变量是否内存安全 (具体分析, 如果生命周期内部消亡就算是安全的)