执行引擎是Java虚拟机最核心的部分。

背景

虚拟机与物理机在执行代码时,执行引擎的区别

  • 物理机
    • 直接建立在处理器、硬件、指令集和操作系统层面上的
  • 虚拟机
    • 自己实现,可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式

Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,大体分为

  • 解释执行
    • 通过解释器执行
  • 编译执行
    • 通过即时编译器产生本地代码

执行引擎流程:字节码文件> 字节码解析> 执行效果

栈帧Stack Frame

用于支持虚拟机进行方法调用和方法执行的数据结构。是虚拟机栈的栈元素

存储内容

方法的局部变量表、操作数栈、动态连接和方法返回地址和附加信息。

说明

在编译时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会收到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

当前栈帧:位于栈顶的栈帧。

一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态,对于执行引擎来说,只有当前栈帧才是有效的,与之关联的方法称为当前方法。

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

局部变量表

Local Cariable Table ,一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

最大容量

在Java程序编译为Class文件时,就在方法的Code属性的max-locals数据项中确定了该方法所需要分配的局部变量表的最大容量。

容量

变量槽(Variable Slot)为最小单位,虚拟机规范要求每个Slot必须能存放一个booleanbytecharshortintfloatreferencereturnAddress类型的数据,一个Slot占用内存大小具体取决于虚拟机实现。

说明

  • 前六种可以按照Java语言去理解,但注意Java语言和JVM中的虚拟机类型时存在本质差别的
  • reference:对象实例的引用
    • JVM规范没有要求长度(32、64)、结构
    • 需要保证两点
      • 从此引用中查找到对象在Java堆中的数据存放的起始地址索引
      • 此引用中查找到对象所属数据类型在方法区中的存储的类型信息,否则无法实现Java语言规范中定义的语法约束
  • returnAddress:指向一条字节码指令的地址
    • 为指令jsrjsr_wret服务
    • 异常处理,现已用异常表代替
  • 对于64位的数据类型,JVM会以高位对齐的方式为其分配两个连续的Slot空间

    • 局部变量表是建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否为原子操作,都不会引起线程安全问题
  • JVM使用索引定位的方式使用局部变量表(0-最大Slot)

    • 32位:n
    • 64位:n、n+1
      • 不允许采用任何方式单独访问其中的某一个,否则在类加载的校验阶段抛异常
  • 方法执行时,JVM使用局部变量表完成参数值到参数变量列表的传递过程

    • 实例方法(非static):
      • 0:方法所属的实例对象的引用(this)
      • 其余按照参数表顺序排列
      • 再分配方法内的变量(顺序、作用域)
  • Slot可复用

    • PC计数器的值超过变量的作用域,那此变量的Slot就可以交给其他变量使用
    • 优点:节省栈帧空间
    • 缺点:影响垃圾收集行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
* 例子1:
* placeHolder在gc后未被回收
* 原因:局部变量表还存有placeHolder的引用
*/
public static void main(String[] args){
byte[] placeHolder=new byte[64*1024*1024];
System.gc()
}
/*
* 例子2:
* placeHolder在gc后被回收
* 更佳做法:
* 控制恰当的作用域来控制变量回收时间
* 补充:
* 如果经过JIT编译优化,是无需手动赋null就可以被回收
*/
public static void main(String[] args) {
{
byte[] placeHolder = new byte[64 * 1024 * 1024];
placeHolder = null;
}
System.gc();
}
/*
* 例子3:
* placeHolder在gc后被回收
* 原因:局部变量表被读写
*/
public static void main(String[] args){
{
byte[] placeHolder=new byte[64*1024*1024];
}
}
int a = 0;
System.gc()
  • 局部变量表中的变量如果声明了但未被赋值是无法使用的
    • 类变量会在“准备阶段”赋系统初始值,在“初始化阶段”赋用户指定的值
1
2
3
4
5
public static void main(String[] args) {
int a;
//编译器报错
System.out.println(a);
}

操作数栈

算术计算时临时数据的存储区域;调用其他方法时进行参数传递

存储内容

虚拟机在操作数栈中存储数据的类型和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,会被转换为int

最大容量

在Java程序编译为Class文件时,就在方法的Code属性的max-stacks数据项中确定了该方法所需要分配的局部变量表的最大容量。

特点

  • 后进先出LIFO栈,访问方式通过入栈出栈

说明

  • 当一个方法开始执行时,操作数栈是空的,会有各种字节码指令往操作数栈中写入和提取内容(入栈、出栈)

    • 例如:加法的字节码指令iadd在运行时操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行指令时,将两个int值出栈相加后,再将结果入栈
  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配

    • 不能出现一个long和一个float使用iadd命令相加
    • 编译器在编译时期验证
    • 类校验阶段的数据流分析中再次验证
  • 概念模型中,操作数栈相互独立;但虚拟机实现中会有一部分重叠来达到在方法调用时共用数据的目的

动态连接

保存指向运行时常量池中该栈帧所属方法的引用,在运行期间将符号引用转化为直接引用

  • 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,作用就是为了支持方法调用过程中的动态链接(Dynamic Linking)。

  • 静态连接:符号引用一部分会在类加载阶段或者第一次使用时转化为直接引用

  • 此处只介绍概念,详情之后会补充。

方法返回地址

保留一些退出方法时,上层方法(调用者)执行状态的信息

退出方法方式

  • 正常完成出口
    • 执行引擎遇到任意一个方法返回的字节码指令,可能会有返回值传递给上层的方法调用者(是否有返回值及类型取决于退出的方法返回指令)
    • 调用者的PC计数器的值可以作为返回地址,栈帧中很有可能会保存这个值
  • 异常完成出口
    • 在方法执行过程中遇到了异常,并且没有在方法体内得到处理(虚拟机异常、athrow字节码指令),不会有返回值
    • 异常处理表来确定返回地址,栈帧中不会保存

退出方法操作

  • 恢复上层方法的局部变量表和操作数栈
  • 如果有返回值,将返回值压入调用者栈帧的操作数栈中
  • 调整PC计数器的值以指向方法调用指令后面的一条指令

附加信息

  • 虚拟机规范允许具体实现中增加规范中没有描述的信息到栈帧之中
    • 调试相关的信息
  • 栈帧信息:动态连接、方法返回地址、附加信息