章节索引 :

JVM 的栈与寄存器

1. 前言

从本节开始,我们对运行时数据区进行讲解,运行时数据区又可以细分为五个模块:栈,堆,寄存器,方法区和本地方法栈,本节我们主要针对讲解栈(包括 Java 栈与本地方法栈)与寄存器。本节主要知识点如下:

  • 了解栈的基本概念及特点,为本节的基础知识;
  • 理解并掌握栈帧的概念以及栈帧的数据结构,并对栈帧结构中的局部变量表,操作数栈,动态链接以及返回地址做详细的讲解,为本节核心内容,需要重点学习;
  • 理解并掌握寄存器的概念及作用,为本节重点内容。

2. 运行时数据区知识回顾

之前我们在讲解 JVM 整体架构的过程中,对运行时数据区进行了总体的概括,运行时数据区又可以细分为五个模块:栈,堆,寄存器,方法区和本地方法栈,如下图所示。

本节我们主要针对讲解栈(Java 栈与和地方法栈)与寄存器(程序计数器),其他 2 个模块,方法区和堆会在后续的课程中进行讲解。

3. 栈的基本介绍

基本概念:Java 栈有两个,分别是虚拟机栈和本地方法栈。这里以虚拟机栈为例,本地方法栈和虚拟机栈基本相同。

栈的特点:对于每个线程,将创建单独的运行时栈。对于每个方法调用,将在栈存储器中产生一个条目,称为栈帧。所有局部变量将在栈内存中创建。栈区域是线程安全的,因为它不共享资源。

  • Java 虚拟机栈是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭);
  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常;
  • Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法执行的同时会创建一个栈帧。对于我们来说,主要关注的栈内存,就是虚拟机栈中局部变量表部分。

Tips:从栈的特点的最后一点可以看到,开发者主要关注的是栈内存,而栈内存的消耗是因为每个方法执行的同时会创建一个栈帧,而占用空间最大的部分就是栈帧的局部变量表部分。后续我们会展开讲解。

4. 栈帧

定义:栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的 java 虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。

栈帧初始化大小:在编译程序代码的时候,栈帧中需要多大的局部变量表内存,多深的操作数栈都已经完全确定了。 因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

栈帧结构:如下图所示,在一个线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。

图片描述

从上图中我们能够看到,栈帧的组成结构,下文我们将对局部变量表,操作数栈,动态链表以及返回地址进行讲解。

5. 栈帧 - 局部变量表

在栈帧中,局部变量表占用了大部分的空间,那么接下来我们看下局部变量表的基本概念与特点。

基本概念:每个栈帧中都包含一组称为局部变量表的变量列表,用于存放方法参数和方法内部定义的局部变量。

特点

  • 局部变量表的容量以变量槽(Variable Slot)为最小单位;
  • 在方法执行过程中,Java 虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程;
  • 局部变量表中的 Slot 是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码程序计数器的值已经超过了某个变量的作用域,那么这个变量相应的 Slot 就可以交给其他变量去使用,节省栈空间。

6. 栈帧 - 操作数栈

操作数栈也是栈帧中非常重要的结构,操作数栈不需要占用很大的空间,那么我们一起来看下操作数栈的作用及特点。

  • 操作数栈是一个后入先出(Last In First Out)栈,方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入和提取的过程,就是入栈和出栈的过程;
  • 操作数栈的每一个元素可以是任意的 Java 数据类型,32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2;
  • 当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,通过一些字节码指令从局部变量表或者对象实例字段中复制常量或者变量值到操作数栈中。

7. 栈帧 - 动态链接与返回地址

动态链接的基本概念及作用如下

  • 每个栈帧都包含一个指向运行时常量池(JVM 运行时数据区域)中该栈帧所属方法属性的引用,持有这个引用是为了支持方法调用过程中的动态链接。
  • 在 Class 文件格式的常量池(存储字面量和符号引用)中存有大量的符号引用(1. 类的全限定名,2. 字段名和属性,3. 方法名和属性),字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。
    这些符号引用一部分会在类加载过程的解析阶段的时候转化为直接引用(指向目标的指针、相对偏移量或者是一个能够直接定位到目标的句柄),这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态链接。

返回地址:返回地址代表的是方法执行结束,方法执行结束有两种方式,我们来具体看下栈帧中返回地址的作用:

  • 当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令(例如:return),这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。
  • 另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是 Java 虚拟机内部产生的异常,还是代码中使用 throw 字节码指令产生的异常,只要在本方法的异常处理器表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
  • 方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整程序计数器的值以指向方法调用指令后面的一条指令等。

8. 寄存器简介

寄存器( PC register )基本概念:每个线程启动的时候,都会创建一个 PC(Program Counter,程序计数器)寄存器。PC 寄存器里保存有当前正在执行的 JVM 指令的地址。

寄存器简介

  • 每一个线程都有它自己的 PC 寄存器,也是该线程启动时创建的。保存下一条将要执行的指令地址的寄存器是:PC 寄存器。PC 寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量;
  • 每个线程都有一个寄存器,是线程私有的,其实就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,以及即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记;
  • 这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 如果执行的是一个 Native 方法,那这个计数器是空的。

9. 寄存器的特点

通过对寄存器的介绍,我们知道,寄存器器是用来存储指向下一条指令的地址,以及即将要执行的指令代码。我们来看下寄存器的特点:

  • 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域; -
  • 在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致;
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 java 方法的 JVM 指令地址:或者,如果是在执行 native 方法,则是未指定值(undefined);
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成;
  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一个条需要执行的字节码指令;
  • 它是唯一一个在 Java 虚拟机规范中没有规定任何 OOM 情况的区域。

10. 小结

本节主要讲解了运行时数据区的栈与寄存器,其中栈又包括了 Java 栈和本地方法栈,因为对于 Java 栈和本地方法栈,内存结构是十分相似的,因此放到一起讲解。本节内容中的核心知识点 - 栈帧,有非常多的概念问题,需要学习者先做了解,在了解的基础上,慢慢的消化。