Loading

JVM-运行时数据区-虚拟机栈

栈主要解决运行时问题,程序如何执行,堆负责解决数据存储问题。

栈帧是一个内存区块,是一个数据集,维系着方法执行过程的各种数据信息。

每个线程都在创建时都会创建一个虚拟机栈,其内部保存着一个个栈帧,一个栈帧对应着一个方法。

-w526

生命周期:和线程一致。
作用:负责Java程序的运行,保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。

栈特性:

  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
  • JVM直接对Java虚拟栈操作只有两个:
    • 每个方法的执行,伴随着入栈
    • 每个方法执行结束后的出栈工作
  • 对于栈来说没有垃圾回收操作
  • 在一条活动线程上,一个时间点只有一个活动的栈帧,被称作为当前栈帧,相对应的方法为当前方法。
  • 执行引擎运行的所有字节码指令只针对当前栈帧
  • 如果该方法调用了其他方法,对应的新栈帧会被创建出来放在栈顶端,成为新的当前栈帧
  • 不同线程之间栈帧不允许相互引用
  • 方法在正常返回或者抛出未处理异常都导致栈帧被弹出

FILO(first in last out )、LIFO(last in first out)

栈会出现的异常:

  • StackOverflowError
    • 固定大小栈超出最大容量
  • OutOfMemoryError
    • 动态大小栈,内存容量不足

JVM相关命令:

  • -Xss 设置线程栈的最大栈空间

栈帧内部结构

-w720

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法返回地址
  • 附加信息

局部变量表

local variables

  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括基本数据类型、对象的引用,以及returnAddress类型
  • 局部变量表是在栈中的,线程私有的,不存在数据安全问题
  • 局部变量表所需容量大小在编译期间确定下来,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间不改变局部变量表大小的
  • 方法嵌套调用次数由栈大小决定
  • 局部变量中的变量只在当前方法调用中有效,方法结束后随着栈帧的销毁随之销毁

Slot

  • 局部变量表最基本存储单元是Slot(变量槽)
  • 在局部变量表中32位以内类型占一个slot(包括returnAdress类型),64位类型(long和double)占两个slot
    • byte、short、char转换为int存储,boolean也转为int(0为false,非0为true)
  • 需要访问局部变量表中一个64位的局部变量,只需前一个索引即可
  • 非static方法中index为0的slot中会存放一个this变量,其他参数按序排序
  • 栈帧局部变量表中slot可重复利用

-w1260

  • 变量分类:

    • 按照数据类型分:
      • 基本数据类型
      • 引用数据类型
    • 按照在类中的声明位置分:
      • 成员变量
        • 在使用前,都经历过默认初始化赋值。
        • 类变量:linking的prepare阶段给类变量赋默认值,initial阶段:给类变量显式赋值即静态代码块赋值
      • 局部变量
        • 在使用前,必须要进行显式赋值,否则编译不通过
  • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

操作数栈

由数组实现

  • 在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈/出栈。

  • 主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时的存储空间。

  • 每个操作数栈由数组实现,有明确的固定长度,在编译期确定。

  • 并非采用索引的方式访问,而是只能通过入栈出栈操作来完成数据的访问

  • 被调用方法有返回值,其返回值将会被被压入当前栈帧的操作数栈中

根据具体代码分析

public void testAddOperation() {
    byte i = 15;
    int j = 8;
    int k = i + j;
}

-w1275

  1. 执行bipush指令将15压入操作数栈
  2. 执行istore_1将15存储在局部变量表1的位置,15出栈(0已保存this)
  3. 执行bipush指令将8压入操作数栈
  4. 执行istore_2将8存储在局部变量表2的位置,8出栈
  5. 执行iload_1、iload_2将8和15分别压入操作数栈
  6. 执行iadd,出栈8和15并相加,再将23压入操作数栈
  7. 执行istore_3将23存储在局部变量表3的位置,23出栈

-w1129
-w867
-w856
-w860

动态链接

指向运行时常量池的方法引用

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

具体举例说明

public class DynamicLinkingTest {

    int num = 10;

    public void methodA() {
        System.out.println("methodA()...");
    }

    public void methodB() {
        System.out.println("methodB()...");
        methodA();
    }

}


javap -v DynamicLinkingTest //反编译

-w853

  • 常量池在运行时会放入方法区,所以叫作运行时常量池, 常量池的作用是:提供一些符号和常量,便于指令的识别。

-w992

方法的调用

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

  • 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
  • 动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。

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

  • 早期绑定:就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
  • 晚期绑定:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

虚方法与非虚方法

静态方法、私有方法、final方法、构造器、父类的方法调用都是非虚方法

  • 普通调用指令
    • invokestatic 调用静态方法
    • invokespecial 调用构造函数、私有、父类方法
    • invokevirtual 调用所有虚方法
    • invokeinterface 调用接口方法
  • 动态调用指令
    • invokedynamic 动态解析出所需调用的方法,然后执行

方法重写的本质

  1. 找到操作数栈顶的第一个元素所执行的对象实际类型,记作C
  2. 如果在类型C中找到与常量中描述相符的方法,则进行访问权限校验,有权限转化为直接引用,无权限返回java.lang.IllegalAccessError异常
  3. 否则,按照继承关系依次从下往上执行第2步骤
  4. 如果始终没有,则抛出java.lang.AbstractMethodError异常

虚方法表

  • 面向对象要频繁使用动态分派,为了提高性能,JVM采用在类的方法区建立虚方法表。使用索引表代替查找

  • 每个虚方法表存放着各个方法的实际入口

  • 虚方法表示在类加载的链接阶段被创建并开始初始化,类的初始化准备完成后,JVM会把该类的方发表也初始化完毕

方法返回地址

  • 存放调用该方法的pc寄存器的值

  • 一个方法的结束,有两种方式:

    • 正常调用完成(执行引擎遇到任何方法返回的字节码指令)
    • 出现未处理的异常,异常调用完成(方法在执行过程中遇到异常,并且这个异常没有在方法内处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称为异常完成退出。)
  • 一个方法在正常调用完成之后需要根据方法返回值的实际数据类型来确定返回指令。

    • 返回指令包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn(long类型)、freturn(float类型)、dreturn(double类型)以及areturn(引用类型),另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。
      -w481
  • 无论通过哪种方式退出,在方法退出后都会返回到最初方法被调用的位置。方法正常退出时,调用者的PC寄存器的值作为返回地址。而通过异常退出的返回地址是通过异常表来确定,栈帧中一般不会保存这部分信息。

  • 正常完成出口和异常完成出口区别在于:通过异常完成出口退出的不会给它的上层调用者产生任何的返回值。

  • 本质上,方法的退出是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者继续执行下去。

附加信息

栈帧中还允许携带与Java虚拟机实现相关的一些附加信息:如对程序调试提供支持的信息。

相关面试题

  • 举例栈溢出的情况?

    • StackOverflowError(通过-Xss设置栈大小,OOM栈空间不足)
  • 调整栈大小,就能保证不出现溢出吗?

    • 不能。(递归的情况下总是会出现栈溢出)
  • 分配的栈内存越大越好吗?

    • 不是,过大会挤占其他内存空间,线程数会变少
  • 垃圾回收是否涉及到虚拟机栈?

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

    • 具体情况具体分析
      -w695
posted @ 2021-03-08 21:50  Vic呼叫89  阅读(66)  评论(0)    收藏  举报