首页 > 技术文章 > Java虚拟机栈

old-cha 2020-06-10 22:11 原文

虚拟机栈

  • Java虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。同样,线程私有,生命周期与线程一致。
  •  每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

928f83fce9a8d9946ad6ffeb41614013.png  

栈的特点:

  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
  • JVM直接对Java栈的操作只有两个:
    • 每个方法执行,伴随着进栈(入栈,压栈)
    • 执行结束后出栈
  • 栈不存在垃圾回收问题

设置虚拟机栈大小参考:(-Xss 参数)
https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE
如何设置:(IDEA)

787808c6123b56317698df0611ea4e1d.png  
9cc8838d433bc8ac7b1283b4b77281dd.png  

栈帧的内部结构

每个栈帧中存储着:

  • 局部变量表 Local Variables
  •  操作数栈 Operand Stack
  • 动态链接 Dynamic Linking(指向运行时常量池的方法引用)
  • 方法返回地址 Return Address(方法正常退出或异常退出的定义)
  •  一些附加信息

82ed7d365a2a6d773ffafd35a20f3695.png   

局部变量表

特点

  • 局部变量表也被称为局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体的局部变量,这些数据类型包括各类基本数据类型、对象引用、已经returnAddress类型。
  • 由于局部变量表是建立在线程栈上的,是线程私有数据,因此不存在数据安全问题。
  • 局部变量表所需的容量大小在编译期确定下来,保存在方法的Code属性的maximum local variable数据项中。在方法运行期间不会更改。
  • 参数值的存放总是在局部变量数组的index0开始,到数组长度-1索引结束。
  • 局部变量表,最基本存储单元是Slot(变量槽)。
  • 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
  • 在局部变量表里,32位的类型只占用一个Slot(包括returnAddress类型),64位的long和double会分配两个连续的Slot,其中byte,short,char在存储前被转为int,boolean也被转为int,0表示flase,非0为true。
  • JVM会为局部变量表中每一个Slot分配一个访问索引,通过索引访问指定变量值。
  • 当一个实例方法被调用时,它的方法参数和方法内部定义的局部变量将会按照顺序被复制到局部变量表的每一个Slot上。
  • 如果需要访问64bit的局部变量值时,只需使用前一个索引即可。
  • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在indxe为0的Slot处,其余参数按照参数表顺序继续排列。

以下面代码为例分析字节码指令:
85fe65273b44f97ce4014d62ef3d1fcb.png  
使用JClassLib查看:
e071c7ed62bfec530195814bf9f5a0f5.png  

  • 为了节省栈帧空间,Slot是可以重复利用的。如果当前字节码PC计数器值超出某个变量作用域,该变量对应Slot可以交给其他变量使用。但因此也涉及到垃圾回收问题。

比如:
4fec796fe7748426f9da778d8f22898d.png  
可见placeholder的槽位被a所占
1a84399736d8bedf33828f1572fe9bfe.png  

 


关于变量:

  • 按照类型分为:基本数据类型,引用数据类型
  • 按类中声明位置分为:

    - 成员变量(使用前经历过默认初始化赋值)
    - 类变量: linking和prepare阶段,给类变量默认赋值 / initial阶段赋予程序员定义初始值
    - 实例变量:随着对象创建在堆空间分配实例变量值,并进行默认赋值
    - 局部变量(定义了没有赋初始值不能使用)


补充说明:

  • 在栈帧中,与性能调优关系最密切的部分就是局部变量表。方法执行时,虚拟机使用局部变量表完成方法传递。
  • 局部变量的表的变量也是重要的垃圾回收根节点,只要局部变量表中直接或间接引用的对象都不会被回收。

操作数栈

  • 操作数栈是后入先出的栈。也称为表达式栈。
  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(Push),出栈(Pop)。
  • 操作数栈,主要用于保存计算过程中间结果,同时作为计算过程中变量临时的存储空间
  • 当一个方法开始执行时,该方法的操作数栈的空的。
  • 每个操作数栈都会拥有一个明确的栈深度用于储存数值,其所需的最大深度在编译器就定义好了,保存在方法的Code属性中,为max_stacks值。
  • 栈中的任何一个元素都可以是Java任意数据类型(32bit占用一个栈单位深度,64bit占2个)。
  • 操作数栈并非采用访问索引的方式来进行数据访问,而是只能通过标准的入栈和出栈完成一次数据访问。
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令
  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,由编译器在编译期间进行验证,同时在类校验阶段的数据流分析还要再次验证。
  • Java虚拟机的执行引擎是基于操作数栈的执行引擎

如下代码为例,store为操作局部变量表,push,add操作的是操作数栈。
64cce9e2ce12c579a69e5614267c18d9.png

栈顶缓存技术

基于栈式架构的虚拟机所使用的零地址更加紧凑,但完成一项操作需要更多入栈出栈操作指令,意味着需要更多的指令分派次数和内存读/写次数。
由于操作数存在内存中,频繁读写会影响运行速度,HotSpot JVM提出栈顶缓存技术,将栈顶元素全部缓存在物理CPU寄存器中,以此降低读写次数,提高引擎执行效率

动态链接

  • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
  • Java源文件被编译为class字节码文件时,所有变量和方法引用都作为符号引用保存在class文件的常量池(Constant Pool)中。比如:描述一个方法调用了另外其他方法时,就是通过常量池中指向方法的符号引用来表示,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

方法返回地址

  • 存放调用该方法的PC寄存器的值
  • 一个方法的结束,有两种方式:1)正常执行结束 2)出现未处理的异常,非正常退出
  • 无论通过哪种方式退出,在方法退出后都返回该方法被调用的位置。方法正常退出时,调用者的PC寄存器的值作为返回地址,即调用该方法的指令的下一条指令地址。程序异常退出时,返回地址通过异常表来确定,栈帧一般不保存。

附加信息

一般把动态链接、方法返回地址与其他附加信息归为一类,统称为栈帧信息。

方法的调用

在JVM中,将符号引用转换为调用方法的直接引用于方法绑定机制相关:

  • 静态链接:

当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。

  • 动态链接:

如果被调用方法在编译期无法确定下来,也就是说只能在程序运行期将调用方法的符号引用转换为直接引用,这种称为动态链接。


对应的方法绑定机制为:早起绑定和晚期绑定。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,仅仅发生一次。

  • 早起绑定:

目标方法在编译期可知,且运行期保持不变,可以将方法与所属类型进行绑定,这样明确被调用方法具体是哪一个,因此使用静态链接方式将符号引用转为直接引用。

  • 晚期绑定:

如果被调用方法在编译期无法确定,只能在运行期根据实际的类型绑定相关方法,这种是晚期绑定。

非虚方法

  • 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
  • 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
  • 其他方法称为虚方法。

虚拟机中提供了以下几条方法调用指令:

  • 普通调用指令:

    1. invokestatic:调用静态方法
    2. invokespecial:调用<init>方法,私有方法,父类方法,解析阶段确定唯一方法版本
    3. invokevirtual:调用所有虚方法
    4. invokeinterface:调用接口方法

  • 动态调用指令:

    5.invokedynamic:动态解析出需要调用的方法,然后执行

前四条指令固话在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(fianl修饰外)为虚方法。

动态类型语言和静态类型语言

在运行期进行检查的是动态类型语言,在编译期就进行检查的是静态类型语言。其实就是说静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息。

Java语言方法重写的本质

1. 找到操作数栈顶的第一个元素(对象引用)所指向的对象的实际类型,记作C。       
2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError。     
3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证。     
4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

IllegalAccess介绍:
程序试图访问或修改一个属性或调用一个方法,这个属性或方法无权访问,一般会引起编译期异常。这个错误如果发生在运行时,说明一个类发生了不兼容的改变。

  • 在面向对象编程中,会频繁使用动态分配,如果每次动态分配过程中都要在类方法元数据中搜索合适的目标就可能影响到执行效率。因此为了提高性能,JVM在方法区建立了一个虚方法表,使用索引表来代替查找。
  • 每个类都有一个虚方法表,表中存放各个方法的实际入口。
  • 虚方法表在加载的链接阶段Linking创建并初始化,类的变量初始值准备完成后,JVM会把该类的方发表也初始化完毕。

栈的相关面试题

  • 举例栈溢出的情况(StackOverfolwError)

  通过-Xss设置栈的大小,内存空间不足扩容导致OOM

  • 调整栈的大小,能保证栈不溢出嘛?

  不能,可能理论上出现的时间更晚,但不一定会保证不溢出

  • 分配的栈内存越大越好嘛?

  不是

  • 垃圾回收是否会涉及到虚拟机栈?

  不会

  • 方法中定义的局部变量是否线程安全?

  具体问题具体分析

 

推荐阅读