主要内容:

  • 介绍虚拟机栈

1、概述

每个线程在创建的时候都会创建一个虚拟机栈,其内部保存了一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。首先要明确:栈是运行时的单位,而堆是存储的单位。

  • 栈解决程序的运行问题,即程序如何执行,或者说程序如何处理数据。
  • 堆解决数据的存储问题,即数据怎么放、放哪里。

1.1 生命周期

与程序计数器一样,Java虚拟机栈也是线程私有的,生命周期与线程相同。

1.2 作用

主管Java程序的运行。Java虚拟机栈保存方法的局部变量、部分结果,并参与方法的调用和返回。

1.3 特点

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

  • JVM对栈的操作只有两个:

    • 方法执行,入栈。
    • 方法结束,出栈。
  • 栈不存在垃圾回收的问题。

1.4 栈中可能出现的异常

Java虚拟机规范允许栈的大小可以动态设置,也可以是固定的大小。

  • StackOverFlow:线程请求的栈容量超过了栈的最大容量。
  • OutOfMemory:栈在动态扩展时,无法申请到足够的内存,或者在创建的时候,没有足够的内存。

1.5 栈的存储单位

  • 栈中的存储单位是:栈帧。
  • 每个方法都有自己各自的栈帧。
  • 栈帧是一块内存区域,是一个数据集,维系着方法执行过程中的各种数据信息。

1.6 栈的运行原理

  • 在一个活动线程中,一个时间点上,只会有一个活动的栈帧,即只有当前正在执行的方法的栈帧是有效的,这个栈帧称为当前栈帧。与当前栈帧对应的方法就是当前方法,定义这个方法的类就是当前类。
  • 执行引擎运行的字节码指令,只针对当前栈帧进行操作。
  • 如果在方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,作为新的当前栈帧。
  • 不同线程中所包含的栈帧是不存在相互引用的,不可能在一个栈帧中引用另外一个线程的栈帧。
  • 如果方法1调用了方法2,在方法1返回之际,会将执行结果传递给栈帧1,接着栈帧2出栈,使得栈帧1成为当前栈帧。
  • 方法有两种返回函数的方式。一种是正常的函数返回(return指令),一种是抛出异常。无论是哪种方式,都会导致栈帧出栈。

1.6 栈帧的内部结构

每个栈帧中存储着:

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

1.6.1 局部变量表

  • 局部变量表也被称为局部变量数组或本地变量表。
  • 局部变量表是建立在线程的栈上,属于线程私有数据,不存在数据安全问题。
  • 主要用于存储方法参数和定义在方法体内的局部变量。包括编译期可知的8种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用、returnAddress类型。
  • 局部变量表的大小是编译期确定的。在方法运行期间是不会改变局部变量表的大小的。
  • 方法嵌套调用的次数是由栈的大小决定的。一般来说,栈越大,方法嵌套调用次数越多。对于一个函数而言,参数和局部变量越多,局部变量表就越膨胀,栈帧就越大,进而函数调用就会占用更多的栈空间,嵌套调用次数就越少。
  • 局部变量表中的变量只在当前方法调用中有效。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

1.6.2 操作数栈

  • 操作数栈主要用保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。
  • 操作数栈是JVM执行引擎的一个工作区,当一个方法开始执行时,一个新的栈帧会被创建出来,此时这个方法的操作数栈是空的。
  • 每一个操作数栈都有一个明确的栈深度,在编译期就确定了。
  • 操作数栈中的元素可以是任意的Java数据类型。
  • 如果调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
  • 另外,我们说的:Java虚拟机的解释引擎是基于栈的执行引擎,其中栈就是指的操作数栈。
  • 栈顶缓存技术:HotSpot虚拟机的执行引擎是基于栈的架构,并非是基于寄存器的架构,但这并不代表HotSpot虚拟机没有用到寄存器资源。寄存器是物理CPU的组成部分之一,同时也是CPU中非常重要的高速存储资源,主要是用来缓存本地机器指令、数值和下一条要执行的指令地址等数据。一般来说,寄存器的读写速度甚至比内存的读写速度快几十倍不止,不过寄存器资源非常有限,而且不用平台下的CPU寄存器的数量是不同和不规律的。基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也意味着需要更多的指令分派次数和内存读写次数。由于操作数是存储在内存中的,因此频繁地执行内存的读写操作必然会影响执行速度。为了解决这个问题,设计者们提出了栈顶缓存技术:将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。

1.6.3 动态链接

  • 每一个栈帧内部都包含了一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
  • Java源文件被编译到字节码文件时,所有的变量和方法都作为符号引用Class文件的常量池中。动态链接的作用就是将这些符号引用转化为调用方法的直接引用。
  • Class文件的编译过程中不包括传统编译器中的连接步骤,一切方法调用在Class文件里面存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。也就是需要在类加载阶段,甚至是运行时才能确定方法的直接引用。

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

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

对应的方法的绑定机制分为:

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

什么是动态类型语言?
动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,在编译期就进行类型检查过程的语言是静态类型语言。说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。例如:python、Javascript、Ruby语言是动态类型语言。

虚方法和非虚方法:

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

虚方法表:

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

1.6.4 方法返回地址

  • 方法返回地址中存放的是:调用该方法的上级方法的PC寄存器中所存的值。
  • 一个方法的结束,有两种方式:正常执行完成,或者出现未处理的异常,程序非正常退出。无论哪种方式退出,在方法退出后都返回到该方法被调用的位置。
  • 执行引擎遇到任何一个方法返回的字节码指令时,会有返回值传递给上层调用者,称为正常完成出口。
  • 在方法的执行过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,称为异常完成出口。
  • 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值
  • 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈,将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

1.6.5 附件信息

栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息,但这些信息取决于具体的虚拟机实现。