jvm虚拟机栈:Java程序的执行框架

虚拟机堆栈概述

在 jvm初识 中提到了java程序运行时数据区,其中运行时数据区中涵盖了虚拟机栈的概念,很多人会不太清晰栈和堆的区别, 这里对这两个也做一下区别的对比。本篇着重还是学习jvm虚拟机栈。

虚拟机栈的基本概念

虚拟机栈是每个线程私有的内存区域,用于存储方法的执行信息。每个方法在执行的同时都会创建一个栈帧(Stack Frame),栈帧包含了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息。

jvm虚拟机栈:Java程序的执行框架_第1张图片

堆的基本概念

堆是Java虚拟机中用于存储对象实例的区域。在堆中分配的内存由垃圾回收器进行管理,堆的大小在程序启动时就被确定,与虚拟机的生命周期一致。

虚拟机栈与堆的区别

- 存储内容

虚拟机栈: 主要存储线程执行的方法信息、局部变量、操作数栈等,是方法执行的工作区域。

堆: 主要用于存储对象实例和数组,是动态分配内存的地方。对象在堆中分配,而引用变量存储在栈上。

- 生命周期

虚拟机栈: 生命周期与线程相同,每个线程都有自己的虚拟机栈,栈的生命周期随着线程的创建和销毁而变化。

堆: 生命周期通常与Java虚拟机的生命周期一致,由垃圾回收器负责管理堆中的内存。

- 异常情况

虚拟机栈: 可能发生栈溢出异常(StackOverflowError),当栈的深度超过虚拟机所允许的最大深度时。

堆: 可能发生堆溢出异常(OutOfMemoryError),当堆中无法分配足够的内存时。

虚拟机栈与堆的作用

虚拟机栈: 负责管理方法的调用和返回信息,存储局部变量,执行方法中的操作。

堆: 用于存储对象实例和数组,提供动态分配内存的机制。

虚拟机栈的特点

栈是一种快速有效的分配存储方式,其访问速度仅次于程序计数器。

执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。

在Java虚拟机(JVM)中,对Java栈的操作主要涉及两个阶段:入栈和出栈。

  1. 入栈(进栈、压栈): 每当一个方法被执行时,对应的栈帧会被创建并压入虚拟机栈。这个栈帧包含了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息。方法的参数和局部变量都被分配到栈帧的局部变量表中。

  2. 出栈: 当方法执行结束时,对应的栈帧将被弹出,栈帧的局部变量表等信息也随之销毁。方法返回地址被读取,控制流程返回到调用该方法的地方。

jvm虚拟机栈:Java程序的执行框架_第2张图片

jvm虚拟机栈:Java程序的执行框架_第3张图片

栈的特点使其在方法调用和返回的过程中提供了高效的内存分配和释放机制。由于栈是线程私有的,每个线程都有独立的虚拟机栈,因此不存在多线程之间的内存冲突问题。这也使得栈在并发执行时更加高效。

与堆不同,栈不存在垃圾回收的问题。由于栈的生命周期与方法的调用和返回相对应,栈上的内存会在方法执行结束时自动释放。这避免了垃圾回收器的介入,使得栈的管理更为简单和直观。

然而,栈也存在一定的限制,如栈的深度限制。当栈的深度超过了虚拟机所允许的最大深度时,可能会导致栈溢出异常。因此,在编写程序时,需要注意递归调用等可能导致栈溢出的情况。

设置栈内存大小

我们可以使用参数 -Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度

-Xss1m
-Xss1k

栈运行原理

不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。

如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

栈帧的内部结构

局部变量表(Local Variables)

操作数栈(operand Stack)(或表达式栈)

动态链接(DynamicLinking)(或指向运行时常量池的方法引用)

方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)

一些附加信息

局部变量表

局部变量表是栈帧中最重要的数据结构之一。它用于存储方法的局部变量,包括方法参数和在方法内部定义的局部变量。局部变量表的容量在编译时确定,并且对于基本数据类型和对象引用都占据一个槽位slot。

局部变量表定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。

由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。

jvm虚拟机栈:Java程序的执行框架_第4张图片

槽位slot
Slot的基本特性
  1. 基本数据类型和引用类型存储规则: 32位以内的类型(包括returnAddress类型)占用一个Slot,而64位的类型(如long和double)占用两个Slot。

  2. 类型转换规则: byte、short、char在存储前会被转换为int,而boolean也被转换为int,其中0表示false,非0表示true。long和double则占据两个Slot。

  3. 访问索引分配: JVM为局部变量表中的每一个Slot分配一个访问索引,通过这个索引可以成功访问到局部变量表中指定的局部变量值。

方法调用过程中的局部变量表

在实例方法被调用时,方法参数和方法体内部定义的局部变量会按照顺序被复制到局部变量表的每一个Slot上。64位的局部变量值只需使用前一个索引即可访问,例如,访问long或double类型变量。

构造方法和实例方法中的this引用

如果当前帧是由构造方法或者实例方法创建的,对象引用this将会存放在index为0的Slot处。其余参数按照参数表顺序继续排列。这意味着this引用是局部变量表中的第一个元素。

举例说明

为了更好地理解上述规则,我们以一个简单的Java方法为例:

public class LocalVariableExample {
    private int instanceVariable;

    public int add(int x, int y) {
        int sum = x + y + this.instanceVariable;
        return sum;
    }
}

add方法中,局部变量表的存储过程如下:

  • index 0: this引用
  • index 1: x参数
  • index 2: y参数
  • index 3: sum局部变量
  • index 4: instanceVariable实例变量

这样的存储方式清晰地展示了局部变量表在方法调用中的组织结构。

操作数栈

操作数栈是栈帧中的一部分,用于存储方法执行过程中的操作数。例如,在方法调用、算术运算等过程中产生的临时数据都存储在操作数栈中。栈的特点是后进先出(LIFO),方法执行时操作数栈的操作是基于栈顶的。

操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)

  • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈

  • 比如:执行复制、交换、求和等操作

jvm虚拟机栈:Java程序的执行框架_第5张图片

栈顶缓存技术

在基于栈式架构的虚拟机中,使用零地址指令可以使指令更加紧凑,但执行一项操作通常需要更多的入栈和出栈指令。这导致执行过程中产生更多的指令分派和内存读/写操作,对性能产生影响。

为了解决这个性能问题,HotSpot JVM引入了栈顶缓存(Top Of Stack Caching)技术。这项技术的核心思想是将栈顶元素缓存到物理CPU的寄存器中,从而减少对内存的频繁读/写操作,提高执行引擎的执行效率。

核心优势
  1. 减少内存读/写次数: 将栈顶元素缓存到寄存器中,减少了对内存的读/写次数。因为栈顶元素是执行引擎操作的主要数据,减少了与内存的交互,提高了执行速度。

  2. 加速指令执行: 栈顶缓存使得执行引擎可以更快速地访问栈顶元素,而无需频繁地访问内存。这对于频繁的操作尤其重要,如方法调用、循环等。

  3. 提升整体执行效率: 通过减少内存访问的开销,栈顶缓存技术有助于提升整体的执行效率,使得虚拟机在处理栈操作时更加高效。

实现原理

实现栈顶缓存技术的关键在于将栈顶元素及时缓存到寄存器中,并在需要时更新寄存器中的值。这通常需要对虚拟机的执行引擎进行优化,确保在执行指令时能够充分利用寄存器中的缓存。

动态链接

在Java程序中,当一个方法被调用时,需要知道该方法的具体位置,即方法在内存中的地址。但在Java源文件被编译成字节码文件时,方法的地址是未知的,因此无法直接在编译时确定方法的位置。为了解决这个问题,Java引入了符号引用的概念。

符号引用(Symbolic Reference): 在class文件的常量池中,所有的变量和方法引用都以符号引用的形式保存。这种引用不包含具体的内存地址,而是通过符号来表示,比如方法的名称、参数类型等。

动态链接(Dynamic Linking): 当一个方法被调用时,它的符号引用需要在运行时转换为具体的内存地址,这个过程就是动态链接。为了支持动态链接,每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。这个引用指向方法在常量池中的符号引用。

举例来说,如果一个方法A在代码中调用了另外的方法B,编译时并不知道方法B的具体地址,而是将方法B的符号引用保存在常量池中。在方法A被调用时,通过动态链接,方法B的符号引用会被解析为具体的内存地址,从而实现方法调用的具体跳转。

jvm虚拟机栈:Java程序的执行框架_第6张图片

方法返回地址

在Java虚拟机中,一个方法的退出方式有两种:正常完成出口和异常完成出口。无论通过哪种方式退出,方法都会返回到该方法被调用的位置。

正常完成出口

  1. 返回地址存放: 当一个方法正常完成执行时,调用者的 PC 寄存器的值作为返回地址。这个返回地址指向调用该方法的指令的下一条指令的地址。

  2. 返回指令: 方法正常退出时,根据返回值的数据类型,使用相应的返回指令。例如,ireturn(返回int类型)、lreturn(返回Long类型)、freturn(返回Float类型)、dreturn(返回Double类型)、areturn(返回引用类型)、return(声明为void的方法的返回指令)。

异常完成出口

  1. 异常处理表: 在方法执行过程中,如果发生异常并且没有在方法内进行处理,即在异常表中没有匹配的异常处理器,就会导致方法退出。异常完成出口的返回地址通常需要通过异常处理表来确定。

  2. 异常表存储: 异常处理表存储在方法的字节码中,用于指导在发生异常时,如何进行异常处理。它包含了异常类型、异常处理代码的起始地址等信息。

你可能感兴趣的:(jvm,jvm,java,开发语言)