JVM虚拟机(HotSpot)
第五章:虚拟机栈
一丶虚拟机栈概述
优点:指令集小,编译期容易实现。
缺点:性能下降,实现同样的功能需要更多的指令。
Java虚拟机栈是什么:每一个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法调用。线程是私有的!
生命周期:生命周期和线程一致
作用:主管Java程序的运行,他保存方法的局部变量(8中基本数据类型,对象的引用地址),不封节后,并参与方法的调用和返回。
{ 局部变量 VS 成员变量 }
{ 基本数据类型 VS 引用数据类型(类,接口,数组…) }
栈的特点(优点)
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
- JVM直接对Java栈的操作只有俩个:
- 每个方法执行,伴随着进栈(入站,压栈)
- 执行结束后的出栈工作
- 对于栈来说不存在垃圾回收问题
虚拟机栈基本信息
- 设置栈内存大小:使用参数-Xss 来设置
二丶栈的存储单位
栈中存储的是什么:
- 每个程序都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在
- 在这个线程上正在执行的每一个方法都有各对应的一个栈帧
- 栈帧是一个内存区块,是一个数据集,维系这方法执行过程中的各种数据信息
栈运行原理:压栈,出栈。“先进后出,后进先出”
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。只有当前正在执行的方法的栈帧是有效的,这个栈帧被称为当前栈帧,与之对应的是当前方法,定义这个方法的类就是当前类
不停的线程中所包含的栈帧是不允许存在相互应用的,即不可能在一个栈帧值中引用另外一个线程的栈帧。可以使用同一个进程(堆空间)
如果当前方法调用了其他方法 ,方法返回值际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java的返回函数方式:都会使得栈帧被弹出
- 正常的函数返回,使用return指令
- 抛出异常
栈帧的内部结构:
- 局部变量表
- 操作数栈(表达式栈)
- 动态连接(指向运行时常量的方法引用)
- 方法返回地址(方法正常退出或者异常退出的定义)
- 一些附加信息
三丶局部变量表(局部变量数组/本地变量表)(重点
)
局部变量表
-
定义为一个数字数组,主要用于存储方法参数和定义在方法体内部的局部变量,这些数据类型包括基本数据类型,对象引用,一级returnAddress类型
-
由于局部变量表是建立在线程的栈上,是线程私有数据,因此不存在数据安全问题
-
局部变量表所需要的容量大小是在编译期间确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的
-
(温习) 方法嵌套调用的次数有栈的大小决定。一般来说,栈越大,方法嵌套调用的次数越多。
-
(温习)局部变量表中的变量只在当前方法调用中有效。当前方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁
关于Slot
-
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
-
局部变量表,最基本的存储单元是Slot(变量槽)
-
局部变量表中存放着编译期可知的各种基本数据类型(8种),引用数据类型(reference),returnAddress类型数据的变量
-
在局部变量表里面,32位以内的类型只占用一个slot(包括returnAddress类型)64位的类型(long和double)占用俩个slot。
- byte,short,char在存储前被转换为 int,boolean 也被转换为int,0标识flase,非0标识true
- long 和 double 则占据俩个 Slot
-
当一个实例方法被调用的时候,他的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上
-
如果需要访问局部变量表中的一个64bit的局部变量事务,只需要使用前一个索引即可
-
如果当前帧是有构造方法或者实例方法创建的,那么该对象引用this将会存放到index为0的slot处,其余的参数会按照表顺序继续排列(非静态方法可以使用this变量 反之不可以使用)
slot重复利用
栈帧中的局部变量中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的
静态变量与局部变量的对比
- 参数表分配完毕之后,在根据方法体内定义的变量的顺序和作用域分配。
- 变量表有俩次初始化的聚会,第一次实在 “准备阶段” 执行系统初始化,对类变量设置0值,另一次在 “初始化” 阶段,富裕程序员在代码中定义的初始值
- 和类变量初始化不同的是,局部变量表不存在系统出书化的过程,这意味着一旦定义了局部变量表则必须认为的初始化,否则无法使用。
- 例如:
补充: 变量的分类:
- 按照数据类型分:①基本数据类型 ②引用数据类型
- 按照在类中声明的位置分:
- ①成员变量:在使用前都要经历默认初始化赋值。 类变量:linking 的 prepare 阶段,给类变量默认赋值 ----> initial 阶段给变量显式赋值即静态代码块赋值。 实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值。
- ②局部变量 :在使用前并需要进行显示赋值!否则,编译不通过
在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行,虚拟机使用局部变量表完成方法的传递
局部变量表的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
四丶操作数栈(重点
)
栈可以看作一个满足特殊条件的一个数组或者是列表
定义:每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以称之为表达式栈
-
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)
-
操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中变量零食的存储空间。
-
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这-个方法的操作数栈是空的
-
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法Code属性中,为Max_stack的值。
-
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
-
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
-
Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是做操作数栈。
五丶代码追踪
- 将 15 压入栈中
- 将 15 取出来(出栈)并放入到局部变量表中 _1代表索引为 1 的位置(因为该方法不是静态方法,所以第 0 位是 this 变量)
- 将 8 压入栈中
- 将 8 取出来(出栈)并放入到局部变量表中 _2代表索引为 2 的位置
- 从局部变量表中将 15 取出来 ,放入到操作数栈
- 从局部变量表中将 8 取出来 ,放入到操作数栈
- 进行 iadd 操作(加法操作) 将结果压入操作数栈中,
- 将 23 取出来(出栈)并放入到局部变量表中 _3代表索引为 3 的位置
- 然后 return 结束程序的执行 (无返回值情况 )
六丶栈顶缓存技术
将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率
七丶动态链接 ( 指向运行是常量池的方法引用 )
帧数据区
:方法返回地址,动态链接,一些附加信息
每一个栈帧内部都包含一个指向 运行是常量池
中该栈帧所属方法的引用。包含这个应用的目的就是为了支持当前方法的代码能够实现动态链接。
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
为什么需要常量池:常量池的作用,就是为了提供一些符号和常量,便于指令的识别。
八丶方法的调用:解析与分派
方法的调用
在JVM中,将符号引用转化为调用方法的直接引用与方法绑定机制相关。
静态链接
:当一个字节码文件被装载进JVM内部时,如果被调用的目标在编译期间可知,且运行期间不变。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
动态链接
:如果被调用的方法在编译期间无法被确定下来,只能够在程序运行期间调用此方法的符号引用转化为直接引用,由于这种引用转化过程具备动态性,因此也就被称之为动态链接
方法的绑定
对应的方法的绑定机制为:早期绑定和晚期绑定。绑定是一个字段,方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
早期绑定
:早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
晚期绑定
:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
虚方法与非虚方法
非虚方法:
- 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
- 静态方法,私有方法,final方法,实例化构造器,父类方法否是非虚方法
其余的方法称为虚方法
虚拟机中提供了以下几条方法调用指令:·
普通调用指令:
1.invokestatic
:调用静态方法,解析阶段确定唯一方法版本
2. invokespecial
:调用方法、私有及父类方法,解析阶段确定唯一方法版本
3. invokevirtual:调用所有虚方法
4. invokeinterface:调用接口方法
·动态调用指令:
5. invokedynamic:动态解析出需要调用的方法,然后执行
前四条指令固化在虚拟机内部,方法的调用执行不可人为千预,而invokedynamic指令则支持由用户确定方法版本。其中的 invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个
invokedynamic指令,这是Java为了实现『动态类型语言』支持而做的一种改进。
但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
动态类型语言和静态类型语言
: 动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征
虚方法表
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,`为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
九丶方法返回地址
方法返回地址:
- 存放调用该方法的pc寄存器的值。
- 一个方法的结束,有两种方式:>正常执行完成
①出现未处理的异常 ②非正常退出 - 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
十丶一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。
十一丶栈的相关面试题
栈中可能出现的异常:
Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选择。如果线程请求分配的栈容量超过Java虚拟机允许的最大容量,Java虚拟机将会抛出一个
StackOverFlowError
异常 - 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足狗的内存,或者在创建新的线程时没有足够的内存取去创建对应的虚拟机栈,那Java虚拟机将会抛出一个
OutOfMemoryError
异常。 - 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值
。
栈的相关面试题
- 举例栈溢出的情况? (
StackOverflowError
)
通过-Xss设置栈的大小,OOM - 调整栈大小,就能保证不出现溢出吗?
不可以保证,只能是保证当前情况下不会发生溢出,但是不能保证别的情况下。 - 分配的栈内存越大越好吗?
不是。对当前可能有益,但是在有限的资源内会占用其他的空间。 - 垃圾回收是否会涉及到虚拟机栈?
不涉及 存在Error(溢出情况) 不存在GC(垃圾回收) - .方法中定义的局部变量是否线程安全?
具体问题具体分析!
何为线程安全:如果只有一个线程才可以操作此数据,那就是安全的。如果有多个线程操作此数据,则此数据是共享数据,不考虑同步机制的情况下会存在安全问题。
回顾
:
程序计数器(PC) 不存在Error(溢出情况) 不存在GC(垃圾回收)
虚拟机栈(JVM) :存在Error(溢出情况) 不存在GC(垃圾回收)
本地方法栈(NM)::存在Error(溢出情况) 不存在GC(垃圾回收)
堆空间(Heap)::存在Error(溢出情况) 存在GC(垃圾回收)
方法区(Method Area)::存在Error(溢出情况) 存在GC(垃圾回收)