2.1 概述
2.1.1 栈和堆
栈管运行,堆管存储
2.1.2 java虚拟机栈是什么
java虚拟机栈是线程私有的,每个线程启动的时候都会创建一个虚拟机栈,对应着java方法的调用
2.1.3 作用
主管程序的运行,保存方法的局部变量(基本数据类型可以直接存储,引用数据类型存储引用地址),部分结果,参与方法的调用的返回
2.1.4 栈的特点
- 栈是一种快速有效的存储方式,仅次于PC register
- 栈不存在垃圾回收问题,存在OOM
- jvm对栈的操作只有两种
- 随着方法的执行 入栈
- 方法执行完毕 出栈
2.2 常见问题
2.2.1 虚拟机栈可能出现的异常
jvm规范是允许虚拟机栈的大小是固定的或者动态的
- 固定 如果线程创建的时候请求分配的栈容量超过了java虚拟机栈允许的最大容量,抛出 StackOverflowError异常
- 动态 如果在动态扩展的时候无法获取到足够的内存,或者线程申请虚拟机栈的时候没有足够的内存创建就会抛出 OutOfMemoryError异常 OOM
2.2.2 设置栈的大小
-Xss 设置栈的大小
2.3 栈的存储单位
栈的存储单元是栈帧
java一个方法与一个栈帧相对应
栈帧是一个内存区块,维系着方法执行过程中的各种数据信息
2.3.1 栈的原理
- jvm对栈的操作只有两种 压栈和出栈 遵循FILO/LIFO(先入后出/后入先出)原则
- 同一线程同一时间点只有一个栈帧是有效的,也就是位于栈顶的栈帧,称为当前栈帧, 与当前栈帧对应的方法就是当前方法,与当前方法对应的就是当前类
- 执行引擎执行的指令也是当前栈帧进行操作,PC Register存储的也是当前栈帧的指令地址
- 如果在当前方法中调用了新的方法,那么新的栈帧就被创建出来,成为新的当前栈帧
- 不同线程中包含的栈帧是不允许存在相互引用的,不可能在一个栈帧中引用另外一个线程的栈帧
- 方法有两种返回返回的方式,不管是哪种返回都会导致栈帧弹出
- 方法执行完毕,正常返回
- 抛出异常返回
2.3.2 栈祯的存储结构
- 局部变量表 Local Variables
- 操作数栈 Operand Stack
- 动态链接 Dynamic Linking
- 方法返回地址 Return Address
- 附加信息
2.3.2.1 局部变量表
- 局部变量表是一个数字数组,存储方法参数和方法体内的局部变量,包括8种基本数据类型,对象引用和returnAddress类型(返回类型)
- 局部变量表线程安全,因为存放在栈帧中,是线程私有的
- 局部变量表的大小是编译期就确定的,运行期不会再改变
- 方法嵌套调用的次数由栈的大小决定,也就是栈内栈帧的多少由栈的大小决定
- 当方法执行完毕后,栈帧销毁,局部变量表也随之销毁
例子:
package com.zy.study04;
/**
* @Author: Zy
* @Date: 2021/7/28 11:36
*/
public class StackFrameTest {
public static void main(String[] args) throws CloneNotSupportedException {
StackFrameTest test = new StackFrameTest();
test.clone();
int num = 1;
}
}
使用jclasslib查看局部变量表
可以看到局部变量表中存储了main方法的两个变量和一个形参,所以局部变量表的大小为3
变量槽slot,局部变量表的基本单位
局部变量表中32位以内占用一个slot(包括引用数据类型),64位的占用两个slot(long和double)
局部变量表与类变量的对比:
- 变量的分类
- 变量按数据类型分类:
- 基础数据类型
- 引用数据类型
- 变量按作用域分类:
- 成员变量
- 类变量即静态变量
- 实例变量
- 局部变量
- 对比局部变量和类变量:
- 类变量经历linking/prepare阶段进行默认赋值,initaizal阶段进行初始化赋值(静态代码块) 局部变量不会默认赋值,在使用前必须显式赋值
补充说明:
局部变量表的调优是性能调优的重要部分,因为局部变量表占据栈帧的很大一份空间,而且被局部变量表直接引用或者间接引用的对象都不会被回收,涉及到垃圾回收算法的垃圾回收根节点
2.3.2.2 操作数栈(Operand Stack)
操作数栈是基于的数组实现的栈结构(后入先出)
操作数栈: 在方法执行中,根据字节码指令,往操作数栈中写入数据/提取数据,也就是入栈/出栈操作(执行引擎).
- 操作数栈主要用于保存计算过程的中间结果,同时也作为计算过程中变量的临时存储空间
- 操作数栈在就是jvm执行引擎的一个工作区,当刚开始执行一个方法的时候,创建了一个栈帧,这个时候操作数栈是空的(数组深度已经有了)
- 与局部变量表相同的是,操作数栈的深度在编译期也已经确定好了,运行时不能修改操作数栈的深度 方法的Code属性的Max_stack属性
- 同样的32bit的类型占用一个栈单位,64位占用两个栈单位
- 操作数栈是栈,只能由入栈/出栈操作,因此,即使操作数栈本质上是一个数组,但是依然不能使用索引来访问操作数栈中的数据
- 如果当前被调用的方法带有返回值时,返回值会被压入当前栈帧的操作数栈,并更新pc寄存器的下一条指令
- 操作数栈的元素的数据类型必须与字节码指令的序列严格匹配,这个由编译器在编译期进行验证,并且在类加载阶段的linking/检验阶段再次验证
- jvm的执行引擎是基于栈的 此处的栈就是操作数栈
2.3.2.3 动态链接(Dynamic Linking)
- 每个栈帧内部都包含一个指向运行时常量池中当前的方法的引用,包含整个引用的目的就是为了实现动态链接
- 在java源代码编译成字节码的时候,所有的变量和方法引用都会作为符号引用保存在class的常量池中,动态链接的作用就是将这些符号引用转换为对方法的直接引用
2.3.2.3.1 方法的调用
jvm中方法由符号引用转为直接引用与方法的绑定机制有关
- 动态链接 编译期无法确定的,只能在运行期才能将方法的符号引用转为直接引用,这样情况叫做动态链接
- 静态链接 当一个字节码文件被装入jvm的时候,被调用的方法编译期可知,并且在运行期一直保持不变,这样情况下将方法的符号引用转换为直接引用的过程叫做静态链接
方法的绑定机制:
绑定指的是方法/属性由符号引用转为直接引用的过程,不管什么时期的绑定都只会发生一次
- 晚期绑定 与动态链接对应
- 早期绑定 与静态链接对应
虚方法与非虚方法:
- 虚方法就是动态链接/晚期绑定 非虚方法就是静态链接/早期绑定
- 静态方法,final方法,私有方法,构造器方法,父类方法因为在编译期就能确定具体的调用版本,所以被称为非虚方法,其他方法称为虚方法
方法调用的字节码指令:
- invokeStatic: 调用静态方法
- invokeSpecial: 调用所有方法,私有方法,父类方法,即(除final方法外)的非虚方法
- invokevirtual: 调用所有虚方法 (还包括final方法)
- invokeInterface: 调用所有接口方法
- invokeDynamic: 动态调用 jdk7新增指令
- 主要为了支持动态语言特性(java语言还是一种静态语言)
- 例如 lambda 表达式就是动态调用的,jdk8才有了直接生成动态调用指令的方式
虚方法的调用过程:
虚方法大多数情况都是因为方法的重写,方法重写的本质从方法调用的角度来看:
- 操作数栈栈顶的第一个元素执行对象的类型,记作C
- 调用C的方法时,通过查询常量池中是否有类型符合,简单名称符合的方法,如果有进行权限校验,校验通过则返回此方法的引用
- 如果不符合,则按照继承关系查找C的父类,以此进行方法判断,如果符合,就返回方法的引用,否则一直执行
- 如果直到最后一个父类也没有合适/符合的方法,抛出异常 AbstractMethodError
由于上述循环查询父类的方式影响性能,为了提高性能,所以在类的方法区建立了一个 虚方法表
虚方法表建立后,就可以根据虚方法表获取要调用方法的引用,不用再递归获取.
虚方法表中,如果重写过的方法,指向的就是子类重写的方法的本质,如果没有重写方法,方法就还是指向父类的方法
2.3.2.4 方法返回地址 Return Address
存放该方法(栈帧)的pc寄存器的值,也就是下一条执行指令
方法退出有两种方式: 1. 正常退出 2. 未处理异常退出
- 正常退出的时候,一般会回到方法被调用的位置,会把pc寄存器中的值存放到方法返回地址中.
- 异常退出的时候,一般通过异常表来确定方法返回地址,不会存放在栈桢中.
- 正常退出与异常退出的区别在于,异常退出不会给上层调用返回任何信息.
2.3.2.5 一些附加信息
根据虚拟机可选,有些虚拟机存在,附件一些与程序调试相关的信息
2.3.3 栈顶缓存技术(Top Of Stack Cashing)
HotSpot JVM的设计者提出的 将栈顶元素全部缓存至物理cpu的寄存器中,减少对内存的读/写,加快执行效率