虚拟机栈
每个线程在创建时都会创建一个虚拟机栈, 其内部保存一个个的栈帧, 对应这一次次的Java方法调用,线程私有的. 生命周期与线程一致. 主管java程序的运行,保存方法的局部变量, 部分结果, 并参与方法的调用和返回
特点
- 内存小,跨平台性,指令集小,编译器容易实现,访问速度仅次于PC寄存器
- JVM直接对Java栈的操作只有 进栈和出栈
- 栈不存在GC问题, 存在OOM, PC寄存器即不存在GC,也不存在OOM
- 缺点是性能下降,实现统一的功能需要更多的指令
可能出现的异常
Java虚拟机规范允许Java栈的大小是动态的或者固定不变的
- 采用固定大小的Java虚拟机栈, 那每一个线程的Java虚拟机栈容量可以在线程创建的时间独立选定. 如果线程请求分配 栈容量超过Java虚拟机栈允许的最大容量,java虚拟机会抛出一个 StackOverflowError 异常
- 如果java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程是没有足够的内存去创建对应的虚拟机栈, 那么java虚拟机将会抛出 OutOfMemoryError 异常
设置栈的大小
-Xss 如 -Xss256k
栈帧弹出方式
- 正常的函数返回,使用return指令
- 抛出异常
栈中存储
- 局部变量表
- 操作数栈
- 动态链接(指向运行时常量池的方法引用)
- 方法返回地址
- 一些附加信息
局部变量表
- 也被称为局部变量数组或本地变量表
- 定义一个数字数组, 主要用于存储方法参数和定义在方法体内的局部变量, 包括各类基本数据类型,对象引用等
- 局部变量表所需的容量大小是在编译器确定下来的, 在方法运行期间是不会改变大小的
- 方法嵌套调用的次数有栈的大小决定.一般来说, 栈越大, 方法嵌套调用次数越多
- 局部变量表中的变量值在当前方法调用中有效,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁
静态变量与局部变量的对比
- 按照数据类型分
- 基本数据类型
- 引用数据类型
- 按照在类中声明的位置分
- 成员变量: 在使用前,都经历过默认初始化赋值
- static 修饰: 类变量, 类加载linking 的准备阶段给类变量默认赋值, 初始化阶段给类变量显式赋值
- 不被 static 修饰: 实例变量,随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
- 局部变量: 在使用前,必须显式赋值,否则编译不通过
- 成员变量: 在使用前,都经历过默认初始化赋值
- 在栈帧中,与性能调优关系最为密切的部分就是局部变量表,在方法执行是,虚拟机使用局部变量表完成方法的传递
- 局部变量表中的变量也是重要的垃圾回收根节点, 只要被局部变量表中直接或间接引用的对象都不会被回收
操作数栈
- 主要用于保存计算机过程的中间结果, 同时作为计算过程中变量临时的存储空间
- 就是JVM执行引擎的一个工作区, 当一个方法开始执行的时候, 一个新的栈帧也会随之被创建出来, 这个方法的操作数栈是空的
- 栈汇总的任何一个元素都可以是任意的java数据类型(32bit类型占用一个栈单位深度)
- 操作数栈并非采用访问索引的方式来进行数据访问,只能通过标准的入栈和出栈操作来完成一次数据访问
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令
- Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
静态链接
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接
动态链接
如果被调用的方法在编译器无法被确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,具备动态性,称为动态链接
- 每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,目的就是为了支持当前方法的代码能够实现动态链接
- 在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件常量池中, 一个方法调用另一个方法时,就是通过常量池中指向方法的符号引用来表示,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
- 常量池的作用就是为了提供一些符号和常量,便于指令的识别
非虚方法
- 如果方法在编译器就确定了具体的调用版本,且在运行时是不可变的,称为非虚方法
- 静态方法,私有方法, final方法,实例构造器,父类方法都是非虚方法
- 其他方法为虚方法
方法返回地址
- 存放调用该方法的PC寄存器的值
- 方法退出后都返回该方法被调用的位置.正常退出时,调用者的PC计数器的值作为返回地址即调用该方法的指令的下一条指令的地址.异常退出时,返回地址是要通过异常表来确定,栈帧中一般不保存这部分信息
- 方法的退出就是当前栈帧出栈的过程.需要恢复上层方法的局部变量表, 操作数栈, 将返回值压入调用者栈帧的操作数栈, 设置PC寄存器值等,让调用方法继续执行下去
- 正常完成出口和异常完成出口的区别在于 : 通过异常完成出口退出的不会给他的上层调用者产生任何的返回值
附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息, 如 对程序调试提供支持的信息
虚拟机栈面试题
举例栈溢出的情况(StackOverflowError)
递归调用等,通过-Xss设置栈的大小
调整栈的大小, 就能保证不出现溢出么?
不能, 调整栈大小只能保证溢出的时间晚一些, 如无线调用递归肯定会溢出
分配的栈内存越大越好么?
不是, 会挤占其他线程的空间
垃圾回收是否会涉及虚拟机栈?
不会
内存区域 | Error | GC |
---|---|---|
程序计数器 | × | × |
本地方法栈 | √ | × |
JVM 虚拟机栈 | √ | × |
堆 | √ | √ |
方法区 | √ | √ |