一、栈可能出现的异常
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError异常。
- 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMenmoryError异常.
二、设置栈内存的大小
我们可以使用参数-Xss
选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
三、栈的存储单位
- 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
- 每个线程上正在执行的每个方法都有各自对应一个栈帧。
- 栈帧是一个内存区块,是一个数据集,维系者方法执行过程中的各种数据信息。
四、栈帧的内部结构
每个栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack) (或表达式栈)
- 动态链接(或指向运行时常量池的方法引用)
- 方法返回地址(或方法正常退出或者异常退出的定义)
- 一些附加信息
4.1、局部变量表
- 局部变量表也被称之为局部变量表或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及
returnAdresss
类型。 - 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的
code
属性的maximum local variables
数据项中。在方法运行期间是不会改变局部变量表的大小的。
4.1.1、关于slot的理解(一)
- 参数值的存放总是在局部变量数组的
index0
开始,到数组长度-1
的索引结束。 - 局部变量表,最基本的储存单元是slot(变量槽)。
- 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
- 在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
-
- byte、short、char在存储之前都被转换为int,boolean也会被转换为int,0表示false,非0表示true
- long和double都占用两个slot
4.1.2、关于slot的理解(二)
- JVM会为局部变量表中的每一个
slot
都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。 - 当一个实例方法被调用的时候,他的方法参数和方法内部定义的局部变量表将会按照顺序被复制到局部变量表中的每一个
slot
上。
-
- 假如说有一个函数
public void a(int c,int d) { int e = 10; long f = 20; }
它在局部变量中是这样的:
- 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如上图的long f变量,就可以访问3下标就可以)
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表继续排列。
-
- 意思就是,当一个类的方法时static,那么它就不能调用this.xxx,因为被static修饰的方法,它的局部变量表中没有this,故不能调用this.xxx。
4.1.3、补充说明
- 在栈帧中,与性能调优关系最为密切的部分就是最前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
- 局部变量表中的变量也是最重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
4.2、操作数栈
- 每一个独立的栈帧中除了包含局部变量表以外,还包含一个先进先出的操作数栈,也可以称之为表达式栈(Expression Stack)。
- 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出站。
-
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数去除栈。使用它们后再把结果压入栈。
- 比如:执行复制、交换、求和等操作。
4.3、栈顶缓存技术
基于栈式架构的虚拟机所使用的的零地址更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这也就意味着需要使用更多的指令分派次数和内存读写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-stack cashing)技术,将栈顶元素全部缓存在物理CPu的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
4.4、动态链接(或指向运行时常量池的方法引用)
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接( Dynamic Linking)。比如: invokedynamic指令。
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用( symbolic Reference)保存在class文件的常量池里比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
4.5、方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制有关。
- 静态链接
当一个字节码文件被装进JVM内部时,如果被调用的目标方法在编译器可知,且运行期保持不变时。这种情况下将调用方法的符合引用转换为直接引用的过程称之为静态链接。
- 动态链接
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
在多态的时候,一般都是使用动态链接
4.6、方法返回地址
- 存放调用该方法的pc寄存器的值
- 一个方法的结束,有两种方式:
-
- 正常执行完成
- 未出现未处理的异常,非正常退出
- 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
五、虚拟机栈常见的面试题
5.1、举例栈溢出的情况(StackOverflowError)
5.2、调整栈大小,就能保证不出现溢出吗?
不能
因为调整栈的大小可以解决暂时栈溢出的问题,但是不排除有些方法在调用的时候会耗费更大的栈空间,因此调整栈大小,不能完全解决栈溢出的问题。
5.3、分配的栈内存越大越好吗?
不是
栈内存分配的内存可以延缓出现StackOverflowError的异常时间,但是绝对避免不了StackOverflowError;而且给栈分配的空间越大,会挤压其他区域的内存分配,会导致其他未知错误的发生。
5.4、垃圾回收是否会涉及到栈空间?
不会
垃圾回收主要处理的对象是堆内存,因为大部分对象都存储在堆中。虚拟机栈内部都存放的是一个个栈帧,都是每个方法必要的数据;如果该方法不用,可以通过出栈的方式释放。
5.5、方法中定义的局部变量是否线程安全?
是
只要不是共享变量,那绝对是线程安全的。