栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据。
正在执行的方法对应的栈帧称为当前栈帧。执行引擎的所有字节码指令只针对栈帧进行操作。
如果当前栈帧调用了新的方法,那么新的方法对应的栈帧会被创建出来并压入栈中成为新的栈帧。
JAVA方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另一种是抛出异常;不管是哪种方式,都会导致栈帧被弹出。
局部变量表:
存在于每个线程的每个栈的每个栈帧中,不存在线程安全的问题。
定义为一个数字数组,主要用于存储方法参数和定义再方法体内的局部变量。这些数据类型包括基本数据类型,对象引用和方法返回地址。
局部变量表所需的容量大小是在编译期就确定下来的,不会在运行期间改变。
在局部变量表中,32位以内的类型占一个slot
(包括方法返回地址),64位的类型占两个slot。byte、short、char在存储前被转为int,boolean也被转为int,0表示false,非0表示true。long和double占据两个slot。
方法嵌套调用的次数由栈的大小决定。如果一个方法的参数和局部变量很多,会使局部变量表膨胀,导致栈帧占用空间增加,压缩嵌套调用的次数。
局部变量表的变量只在当前方法有效,在调用结束后,随着栈帧的销毁而销毁。
slot是局部变量表最基本的存储单元,如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index=0的slot处。slot是可以重用的,当一个局部变量过了期作用域,那么该槽位可会会被回收,以达到节省资源的目的。
局部变量与成员变量的区别:
成员变量:使用前经过初始化赋值;
类变量:在链接的准备阶段,给类变量赋默认值,在初始化阶段(静态代码块),给类变量显性赋值
成员变量:随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
局部变量:在使用前,必须进行显式赋值,否则编译不通过;
在栈帧中,与性能调优关系最为密切的部分就是局部变量表,在方法执行时,虚拟机使用局部表量表完成方法的传递。局部变量表也是重要的垃圾回收根节点,只要在局部变量中直接或间接引用的对象,都不会被回收。
栈帧中有五部分的数据,分别是:局部变量表、操作数栈、方法返回地址、动态链接和一些附加信息。
方法返回地址、动态链接和一些附加信息也统称为帧数据区。
操作数栈(表达式栈):
Java中的操作数栈,使用数组实现。Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈便是指操作数栈。
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。操作数栈是JVM执行引擎的一个工作区,当一个方法开始执行时,对应的新栈帧就会被创建出来,此时为空栈。
每一个操作数栈都拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就已经确定,保存在方法的Code属性中,为max_stack值。
栈中,32bit的数据占一个单位深度,64位占两个深度。
如果被调用的方法有返回值的话,那么其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
i++和++i的区别:i++,先将i从栈顶取出,参与其他计算,然后进行加1再放回栈中;++i,先进行加1操作,再参与其他计算,再放回栈中;
栈顶缓存技术:将栈帧中,操作数栈栈顶的元素全部缓存在CPU的寄存器中,以此减少对内存的读写次数,提高执行效率。
动态链接
字节码文件中定义了一个常量池,动态链接就是指向运行时常量池的方法引用
源文件被编译到字节码文件中时,所有变量和方法引用都作为符号引用保存在class文件的常量池中,动态链接的作用就是为了讲这些符号引用转换为调用方法的直接引用
为什么需要运行时常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。
方法返回地址
存放调用该方法PC寄存器的值。
方法可能正常结束,也可能因异常结束,无论通过哪种方式退出,在方法退出后都返回到该方法的被调用位置。方法正常退出时,调用者的PC寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而因异常结束的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。即异常结束的方法不会给调用者产生任何的返回值
方法执行过程中抛出异常时的异常处理,存储再一个异常处理表,方便再发生异常的时候找到处理异常的代码。在遇到异常时,没有在异常表中搜索到匹配的异常处理,就会导致方法结束。
常见的返回指令有:
ireturn:返回值是boolean、byte、char、short、int
lreturn:long
freturn:foat
dreturn:double
areturn:引用类型
return:void的方法、实例初始化方法、类和接口的初始化方法
附加信息
栈帧允许携带与Java虚拟机实现相关的一些附加信息。例如对程序调试提供支持的信息。
常见面试题
- 栈异常的场景?://todo 待补充
- 调整栈大小,就保证不出现溢出?:对于问题程序,只能影响溢出的时间。对于一些对栈空间要求不高的方法,可能不会溢出。
- 分配栈内存,越大越好?:不一定,对单个方法是利好,但从系统资源利用的角度来看,可能造成严重浪费。
- 垃圾回收是否涉及到虚拟机栈?:不会,栈只有进栈和出栈的操作,不用由GC进行资源回收。
- 方法中定义的局部变量是否线程安全?:方法对应栈帧,栈帧为每个线程内部的私有空间,所以线程安全。