概述
Java虚拟机栈(Java Virtual Machine Stack)。每个线程在创建时都会创建一个虚拟机栈,用来保存一个个栈帧
,对应着一次次的Java方法调用。
生命周期:
虚拟机栈是每个线程私有的,因此生命周期与线程一致
作用:
管理Java程序的运行,虚拟机栈保存方法的局部变量、部分结果,并参与方法的调用和返回。
优点:
1.是一种快速有效的分配存储方式,访问速度仅次于 程序计数器
2.不存在垃圾回收机制(由于操作仅支持pop、push)
开发常见异常:
JVM规范中 允许 Java虚拟栈的大小是动态的 或者 固定不变的
1.当 Java栈 固定大小时,则每一个线程的Java虚拟机栈容量都可以在线程创建时确定。但当线程请求过多,分配的栈容量超过了Java栈允许的最大容量,则JVM会抛出 StackOverflowError
异常
2.当 Java栈 动态扩展时。若在尝试扩展却无法申请到足够的内存,或创建新的线程时没有足够的内存来创建其虚拟机栈时,则JVM会抛出OutOfMemoryError
异常
设置栈大小
-Xss
虚拟机栈的存储单位:栈帧
栈中的数据 以 栈帧
的格式存在。栈帧作为一个内存区块,是一个数据集,用来维系方法执行过程中的各种数据信息。
此时线程正在执行的每个方法都对应着一个栈帧。
运行原理:
在一个活动线程中,一次只有一个活动的栈帧。即正在执行方法的栈帧【又叫栈顶栈帧】是有效的,这个栈帧称之为当前栈帧(Current Frame)
,与当前栈帧相对应的方法就是当前方法(Current Method)
,定义该方法的类便是当前类(Current Class)
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
如果在该方法中调用了其他方法,对应新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧
由于每个线程都含有自己的栈帧,因此栈帧是不允许存在相互引用的。
如果当前方法调用了其他方法,方法返回时,当前栈帧会传回此方法的执行结果给前一个栈帧,然后JVM会丢失当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java方法由两种返回函数的方式,一种是正常的函数返回,return指令。一种是抛出异常。无论是什么方式,都会导致栈帧被弹出。
栈帧内部结构:
栈帧存储着
1.局部变量表(Local Variables)
2.操作数栈(Operand Stack)
3.动态链接(Dynamic)
4.方法返回地址(Return Address)
5.一些附加信息
局部变量表
又名 局部变量数组、本地遍历表
局部变量表定义为一个数字数组
,主要用于存储方法参数和定义在方法体内的局部变量。 这些数据类型包括8大基本数据类型、对象引用(reference)、return Address类型。
局部变量表所需的容量大小是在编译期间 确定下来,并保存于方法中Code属性的 maximum local variables 数据项中。并且在方法运行期间不会改变局部变量表的大小的。
局部变量表中的变量只有在当前方法调用期间有效。原理:虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着对应栈帧的销毁,局部变量表也会随之销毁。
局部变量表中,最基本的存储单元是Slot(变量槽)
其中存放的32位以内的类型(包括returnAddress类型)只占用一个slot,64位类型(long&double)占用两个slot。
JVM会为每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中所指定的局部变量值。
当一个实例方法被调用时,它的方法参数和方法体内定义的局部变量将会被按顺序复制 到局部变量表中的每一个slot中。尽管64位类型占有两个slot,也只需要一个索引(前索引)。
若当前帧是由构造方法或实例方法创建的,则该对象引用this将存放在index[0]的slot处。
栈帧中的局部变量表的slot是可以重复使用的。当一个局部变量过了其作用域,那么在其作用于之后申明的新的局部变量就很有可能复用过期局部变量的槽位,从而达到节省资源的目的。
在栈帧中,与性能调优关系最为密切的部分便是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
变量的相关说明:
1.成员变量:在使用前,会默认初始化赋值。
类变量:在类加载器的Linking的准备阶段,会默认赋值。
实例变量:对象的创建,会在堆空间中分配实例变量空间,并默认赋值。
2.局部变量: 使用前必须显现赋值!否则,编译不通过。
操作数栈
操作数栈(Expression Stack),又名表达式栈。
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
。
在方法执行过程中,操作数栈根据字节码指令,往战中写入数据或提取数据。
某些字节码指令将值 入栈,其余字节码指令将操作数取出栈。使用后再将结果压入栈。比如:执行复制、交换、求和等操作。
操作数栈是JVM执行引起中的一个工作区。当一个方法刚开始执行时,一个新的栈帧随之产生,此时该方法的操作数栈为空。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器便定义好了,保存于 方法中Code属性的max_stack值。
栈可以存放任何Java数据类型。其中32bit占一个栈单位深度。64bit占两个栈单位深度。
若被调用的方法带有返回值,则返回值将会被压入当前栈帧的操作数栈上,并更新PC寄存器中下一条需要执行的字节码指令。
栈顶缓存技术
由于操作数存储在内存中,于是需要经常地执行读/写操作,则必然会影响执行速度。为了解决这个问题,HotSpot JVM地设计者提出 栈顶缓存技术 (Tos,Top-of-Stack Cashing)技术。
将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
动态链接
每一个栈帧为了支持当前方法的代码能够实现动态链接(Dynamic Linking),栈帧内部都包含一个指向 运行时常量池 中关于该栈帧所属方法的引用
。比如:invokedynamic指令。
当Java源文件被编译到字节码文件时,所有的变量和方法引用都会被作为符号引用保存在class文件常量池里。比如:描述一个方法调用另外的方法时,就是通过常量池中指向方法的符号引用来表示的。而动态链接的作用便是为了将这些符号引用转化为调用方法的直接引用。
方法的调用
1.静态链接:
当一个字节码文件被装载JVM内部时,如果被调用的目标方法在编译器可知,且在运气期保持不变时。这种情况下将调用方法的符号引用转换成直接引用的过程。
2.动态链接:
如果被调用的方法在编译器无法确定下来。即只有在程序运行期间将调用方法的符号引用转化成直接引用,而这种引用的转换具备动态性。
绑定机制:
绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程。【仅发生一次】
分类:
早期绑定:
被调用的目标方法如果在编译可知,且运行期保持不变时,便可将该方法与所属类型进行绑定。由于明确了被调用的目标方法,则可以使用静态链接的方式将符号引用为直接引用。
晚期绑定:
如果被调用的方法在编译器无法被确定下来,只能在程序运行期根据实际的类型绑定相关的方法。
虚方法和非虚方法
Java中任何一个普通的方法都具备虚函数的特征。
非虚方法:
如果方法在编译期就确定了具体的调用,并且该调用在运行时是不可变的。(不能被重写、确定下来的方法)
【静态方法、私有方法、final方法、实例构造器、父类方法】
虚方法:
不同于非需方法的方法。
JVM提供了以下方法调用指令:
普通调用指令:
1.invokestatic 调用静态方法,解析阶段确定唯一方法版本
2.invokespecial 调用方法、私有、父类方法,解析阶段确定唯一方法版本
3.invokevirtual 调用所有的虚方法
4.invokeinterface 调用接口方法
普通调用指令的四种指令都固化在JVM内部,调用执行不可人为干预。并且invokestatic指令和invokespecial指令称为非虚方法,其它的[不包含final修饰的] 称为虚方法。
动态调用指令
5.invokedynamic 动态解析出需要调用的方法,然后执行
该动态调用指令由用户确定方法版本。
方法重写的本质:
1.找到操作数栈顶的第一个元素所执行的对象实际类型,记为 C
2.若在类型 C 中 找到与常量中描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束。若不通过,则返回java.lang.IllegalAccessError异常。
IllegalAccessError异常:程序试图访问或修改一个属性、调用一个方法。而该属性、方法并没有足够的权限访问。这是则会引起编译异常。如果在运行期间发生这类错误,则说明一个类发生一兼容的改变。
3.否则,按照继承关系从下往上一次对C的各个父类,进行第二步的搜索与验证。
4.始终都未找到合适的方法,则排除java.lang.AbstractMethodError异常。
虚方法表
由于频繁地使用动态分派。若每次动态分派都需要重新在类的方法元数据中搜索适合的目标,则会影响执行效率。故JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现。听过索引表代替查找。
【每个类中都有一个虚方法表,并且表中存放着各个方法的实际入口】
何时被创建?
在类加载的链接阶段被创建,并初始化。当类的变量初始值准备完成后,JVM会把该类的方法表也初始化完毕。
方法返回地址
用于存放调用该方法的PC寄存器的值。
当方法结束时,都会返回到该方法被调用的位置。此时会出现两种情况
1.而当方法正常退出时,调用者的程序计数器的值将作为返回地址(即调用该方法指令的下一条指令地址。),传递给上层的方法调用者。
– 使用哪一种指令还需根据方法返回值的实际数据类型而定。
– 在字节码指令中,返回指令包含ireturn【返回值是boolean、byte、char、short、int类型】、lreturn、freturn、dreturn、areturn。并且将会有一个reutrn指令来供声明void方法、实例初始化方法、类和接口的初始化方法使用。
2.而通过异常退出时,返回地址则通过异常表确定,而栈帧一般不会保存这部分信息。
二者的区别:异常完成退出的并不会给三层调用者产生任何的返回值。
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复三层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法进行执行。
一些附加信息
栈帧还允许携带与JVM实现相关的一些信息。如:对程序调式提供支持的信息。
注:不一定存在,主要由JVM控制。