JVM-Java virtual machine stack(虚拟机栈)
定义
每个线程创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的方法调用
线程私有
生命周期
与线程保持一致
作用
主管Java程序的运行,保存方法的局部变量(八种基本数据类型和对象的引用地址)、部分结果,并参与方法的调用和返回
栈的特点
快速有效的存储方式,访问速度仅次于pc寄存器
JVM直接对Java栈的操作只有两个:入栈(压栈)、出栈
不存在垃圾回收
栈中可能出现的异常
JVM虚拟机规范允许Java栈的大小是固定的或者是动态的
StackOverFlowError(栈溢出)
- 如果采用固定大小的虚拟机栈,那么每个线程的Java虚拟机栈容量可以在线程创建的时候独立选定.如果线程请求分配的栈容量超出Java虚拟机栈允许的最大栈容量,Java虚拟机会抛出StackOverFlowError异常
OutOfMemoryError(内存泄漏)
如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或在创建新的线程的时候没有足够的内存去创建对应的虚拟机栈,Java虚拟机会抛出OutOfMemoryError异常
栈的存储单位
每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在的
在这个线程上正在执行的每个方法都各自对应一个栈帧
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
栈运行原理
先进先出
在一条活动的线程中,一个时间点上,只会有一个活动的栈帧.即只有当前正在执行的方法的栈帧是有效的,这个栈帧被称之为当前栈帧(Current Frame)
不同线程中所包含的栈帧是不允许存在相互引用的
如果当前方法调用了其他方法,方法返回之际,当前栈帧会回传此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧成为当前栈帧
Java有两种返回函数的方式:
- 正常的函数返回,使用return指令
- 另一种是抛出异常
无论以上哪种方式都会使栈帧被弹出
栈帧
栈帧内部结构
- Local Variables(局部变量表)
- Operand Stack(操作数栈/表达式栈)
- Dynamic Linking(动态链接/指向运行时常量池的方法引用)
- Return Address(方法返回地址)
- 一些附加信息
Local Variables(局部变量表)
局部变量表也被称之为局部变量数组或本地变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量.这些数据类型包括基本数据类型、对象引用、returnAddress类型
不存在数据安全问题
局部变量表所需的容量大小是在编译期确定下来的,并保存在Code属性的maximum local variables数据项中.在方法运行期间是不会改变局部变量表的大小的
方法嵌套调用的次数由栈的大小决定.一般来说,栈越大方法嵌套调用次数就越多.对于一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,他的栈帧就越大,以满足方法调用所需传递的信息增大的需求.进而函数调用就会占用更多的栈空间,导致其嵌套调用的次数就会减少.
局部变量表中的变量只在当前方法调用中有效.方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁
Slot
局部变量表基本单位是slot(槽)
数据类型包括基本数据类型、对象引用、returnAddress类型
在局部变量表中,32位以内的类型只占1个slot,64位的数据类型占2个slot(long和double)
- byte、shot、char在存储前转换为int,boolean也转换为int,0是false,非0是true
- long和double占两个slot
JVM会为每一个slot分配一个访问索引,通过索引即可访问局部变量表中的局部变量值
当一个实例方法被调用的时候,方法参数和局部变量会按照顺序被复制到局部变量表的每一个slot上
如果当前帧是由构造方法或实例方法创建的,那么该对象引用this将会放在index为0的slot处
Slot的重复利用
栈帧中的局部变量表的槽位是可以重复利用的.如果一个局部变量过了其作用域,那么在其作用域之后声明的新局部变量就很有可能复用过期局部变量的槽位,从而达到节省资源的目的
Operand Stack(操作数栈/表达式栈)
数组实现
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
操作数栈就是执行引擎的一个工作区
每一个操作数栈都会有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,在方法的code属性中,为max_stack的值
如果调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中
栈顶缓存技术
讲栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率
Dynamic Linking(动态链接/指向运行时常量池的方法引用)
每一个栈帧内都包涵一个指向运行时常量池中该栈帧所属方法的引用.包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里.动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定即只相关
-
静态链接
对应的方法绑定机制:早期绑定
当一个字节码文件被装载进JVM内部时,如果调用的目标方法在编译期可知,且运行期保持不变的时.这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
-
动态链接
对应的方法绑定机制:晚期绑定
如果被调用的方法在编译期无法被确定下来,也就是说,只能在程序运行期将调用方法的符号引用转换为直接饮用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接
虚方法与非虚方法
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法
- 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法
- 其他方法称之为非虚方法
方法重写的本质
- 找到操作数栈顶的第一个元素所执行对象的实际类型,记作C
- 如果在类型C中找到 常量中的描述符 和 简单名称 都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过则返回java.lang.IllegalAccessError异常
- 否则从上往下依次对C的父类做第二步的搜索校验过程
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
虚方法表
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要在类的方法元数据中搜索合适的目标的话就会影响执行效率.因此为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现,使用索引表来代替查找
分派:
确定执行哪个方法 的过程
每一个类中都有一个虚方法表,表中存放着各个方法的实际入口
虚方法表会在类加载的链接阶段被创建并初始化,类的变量初始值准备完之后,JVM会把类的方法表也初始化完毕.
Return Address(方法返回地址)
存放调用该方法的寄存器的值.
一个方法的结束有两种方式
- 正常执行完成
- 出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置.方法正常退出时,调用者的PC计数器的值作为返回地址,即该方法的指令的下一条指令的地址.而异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息
异常表及JVM对异常的处理:
如果一个方法定义了一个try-catch或try- finally的异常处理,就会创建一个异常表
常表中包含了一个或多个异常处理者(Exception Handler)的信息
- from:可能发生异常的起始点
- to:可能发生异常的结束点
- target:上述from和to之前发生异常后的异常处理者的位置
- type:异常处理者处理的异常的类信息
当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理:
- JVM会在当前出现异常的方法中,查找异常表,是否有合适的处理者来处理
- 如果当前方法异常表不为空,并且异常符合处理者的from和to节点,并且type也匹配,则JVM调用位于target的调用者来处理
- 如果上一条未找到合理的处理者,则继续查找异常表中的剩余条目
- 如果当前方法的异常表无法处理,则向上查找(弹栈处理)刚刚调用该方法的调用处,并重复上面的操作
- 如果所有的栈帧被弹出,仍然没有处理,则抛给当前的Thread,Thread则会终止
- 如果当前Thread为最后一个非守护线程,且未处理异常,则会导致JVM终止运行
引用:详解JVM如何处理异常