8.2 运行时栈帧结构

Java虚拟机以方法作为最基本的执行单元

栈帧(Stack Frame):虚拟机进行方法调用和方法执行背后的数据结构,也是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程

一个栈帧需要分配多少内存,并不会受到程序运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式

当前栈帧(Current Stack Frame):只有位于栈顶的方法才是在运行的,与这个栈帧所关联的方法被称为“当前方法”(Current Method)。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作

8.2.1 局部变量表(Local Variables Table)

存放方法参数和方法内部定义的局部变量,Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量

8.2.2 操作数栈(Operand Stack)

后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。32位数据类型所占的栈容量为1,64位所占的栈容量为2。Javac编译器的数据流分析工作保证操作数栈的深度不会超过max_stacks最大值

8.2.3 动态连接(Dynamic Linking)

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接

8.2.4 方法返回地址

方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等

8.2.5 附加信息

《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关的信息

8.3 方法调用

8.3.1 解析(Resolution)

调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析(主要有静态方法和私有方法两大类)

调用指令

  • invokestatic。用于调用静态方法
  • invokespecial。用于调用实例构造器()方法、私有方法和父类中的方法
  • invokevirtual。用于调用所有的虚方法
  • invokeinterface。用于调用接口方法,会在运行时再确定一个实现该接口的对象
  • invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。前面4条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引导方法来决定的

非虚方法(Non-Virtual Method)

  • 能被invokestatic和invokespecial指令调用,可以在解析阶段中确定唯一的调用版本,在类加载的时候就可以把符号引用解析为该方法的直接引用
  • 静态方法、私有方法、实例构造器、父类方法、被final修饰的方法(invokevirtual)

8.3.2 分派(Dispatch)

1.静态分派

依赖静态类型来决定方法执行版本。最典型应用表现就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的

2.动态分派

在运行期根据实际类型确定方法执行版本.表现为方法重写

invokevirtual指令的运行时解析过程

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

字段没有多态性: 字段不使用invokevirtual指令。哪个类的方法访问某个名字的字段时,该名字指的就是这个类能看到的那个字段

3.单分派与多分派

方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。Java语言是一门静态多分派、动态单分派的语言

4.虚拟机动态分派的实现

使用虚方法表索引来代替元数据查找以提高性能

8.4 动态类型语言支持

8.4.1 动态类型语言

动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,JavaScript、Python等。在编译期就进行类型检查过程的语言,譬如C++和Java等就是最常用的静态类型语言

8.4.2 Java与动态类型

8.4.3 java.lang.invoke包

提供一种新的动态确定目标方法的机制 - 方法句柄(Method Handle)

8.4.4 invokedynamic指令

8.5 基于栈的字节码解释执行引擎

8.5.1 解释执行

8.5.2 基于栈的指令集与基于寄存器的指令集

Javac编译器输出的字节码指令流,基本上是一种基于栈的指令集架构(Instruction Set Architecture,ISA),字节码指令流里面的指令大部分都是零地址指令,它们依赖操作数栈进行工作。与之相对的另外一套常用的指令集架构是基于寄存器的指令集,最典型的就是x86的二地址指令集,如果说得更通俗一些就是现在我们主流PC机中物理硬件直接支持的指令集架构,这些指令依赖寄存器进行工作

基于栈的指令集

  • 可移植
  • 代码相对更加紧凑、编译器实现更加简单
  • 执行速度稍慢

8.5.3 基于栈的解释器执行过程