每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在。
一、栈帧
一个内存区域,是一个数据集,维系着方法执行过程中各种数据信息。
设置虚拟机栈的指令:-Xss256k(K)或者-Xss12m(M),单位不区分大小写
二、栈运行原理
在一条活动线程中,一个时间点上,只会有一个活动的栈帧,即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的。这个栈帧被称为当前栈帧。与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
如果在当前方法中调用了新的方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。
注意:
(1)不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用其他一个线程的栈帧
(2)如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
(3)java方法有两种返回函数的方式,一种是正常函数返回,使用return指令。另一种是抛出异常。
三、栈帧内部结构
1、局部变量表
(1)定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,数据类型包括:基本数据类型,引用类型和returnAddress类型
(2)局部变量表建立在线程栈上,是线程的私有数据,不存在数据安全问题
(3)局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的code属性的maximum local variables数据项,在方法运行期间是不会改变局部变量表的大小。
(4)参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束
(5)最基本的存储单元是slot(变量槽)
32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot
byte、short、char在存储前被转换为int,boolean也被转换为int,0为false,非0为true
i)jvm会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
ii)当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上。
iii)如果要访问局部变量表中的一个64位的局部变量值时,只需要使用前一个索引,比如下图中中要访问double类型,即访问索引4即可。
iV)如果当前帧是由构造方法或者实例方法创建,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
v)栈帧中的局部变量表中槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量有可能复用过期的局部变量的槽位,从而到达节省资源。
jvm为变量b开辟了slot为2的槽,由于b的作用域21行-24行。jvm在给c分配slot时,会重复使用之前分配给b的槽。
(6)方法嵌套调用次数由栈的大小决定。
(7)局部变量表中的变量只有在当前方法调用中有效,在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
(8)类变量在“准备阶段”执行系统初始化,在“初始化”阶段使用程序的显示赋值操作。
但是局部变量不存在系统初始化,即一旦定义了局部变量必须要先人为初始化,否则无法使用。
注意:
(1)在栈帧中,与性能调优关系最为密切的部分是前面提到的局部变量表,在方法执行时,虚拟机使用局部变量表完成方法的传递。
(2)局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或者间接引用的对象都不会被回收。
2、操作数栈
每一个独立的栈帧中除了包含局部变量表之外,还包含一个操作数栈。操作数栈在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈/出栈
(1)某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈
(2)用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
(3)jvm执行引擎的一个工作区域,当一个方法刚开始执行的时候,一个新的栈帧随之被创建出来,这个方法的操作数栈是空的。
(4)每一个操作数栈都会有一个明确的栈深度用于存储数值,所需的最大深度在编译期间就定义好了,保存在方法的code属性中,为max_stack的值,操作数栈是基于数组方式的栈。
(5)栈的任何一个元素都是可以是任意的java数据类型
32bit的类型占用一个栈单位深度,64bit的类型占用两个栈单位的深度。
(6)操作数栈不是用访问索引的方式访问数据,而是通过栈的入栈和出栈操作完成一次数据的访问。
(7)如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新pc寄存器中下一条需要执行的字节码指令。
(8)操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这是由编译器在编译期间进行验证的,同时在类加载过程中的类校验阶段的数据流分析阶段再次验证
例:
3、动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,目的为了支持当前方法的代码能够实现动态链接
在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。
比如:描述一个方法调用了另一个其他的方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了这些符号引用转换为调用方法的直接引用。
4、方法返回地址
(1)存放调用该方法的pc寄存器的值
(2)一个方法的结束,有两种方式
i)正常执行结束
执行引擎遇到一个方法返回的字节码指令(return)会有返回值传递给上层的方法调用者,简称正常完成出口。
一个方法在正常调用完成后,需要哪一种返回指令还需要根据方法返回值的实际数据类型而定。
遇到ireturn(返回值是boolean,byte,char,short,int)
lreturn、freturn、dreturn、areturn(引用类型)和return(void,实例初始化,类和接口的初始化方法的返回)
ii)出现未处理的异常,非正常退出
方法执行过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法的退出。
方法执行过程中抛出异常时的异常处理,存储在一个异常处理表中,方便再发生异常的时候找到处理异常的代码。
(3)无论哪种方式退出,在方法退出后,都会返回到方法该方法被调用的位置。
i)方法正常退出,调用者的pc寄存器的值作为返回地址,即调用该方法的指令的下一条指令地址
ii)异常退出,返回地址要通过异常表来确定,栈帧中一般不会保存这部分信息。
正常返回和异常返回区别:通过异常完成出口退出的不会给它的上层调用者产生任何返回值
(4)本质上,方法的退出就是当前的栈帧出栈的过程,此时需要恢复上层方法的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置pc寄存器值,让调用者方法继续执行下去。
5、附加信息
栈帧允许与java虚拟机实现相关的一些附加信息。比如对程序调试的支持信息等。