在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,可能能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎。
运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
reference类型表示对一个对象实例的引用,虚拟机实现至少都应当能通过这个引用做到两点:
- 从此引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引。
- 此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,否则无法实现Java语言规范中定义的语法约束约束。
Java语言中明确的64位的数据类型只有long和double两种。对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的Slot数量。
局部变量表中第0位索引的Slot默认是用于传递方法所属对象是的引用,在方法中可通过关键字“this”来访问到这个隐藏的参数。其余参数则按照参数表顺序排列,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用与分配其余的Slot。
局部变量与类变量不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,不要认为java中任何情况下都存在诸如整型变量默认为0,布尔型变量默认为false等,如以下代码
1 | //未赋值的局部变量 |
操作数栈(理解是充当寄存器的角色)
操作数栈也常称为操作栈,它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。
- 操作数栈的每一个元素可以是任意的Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
方法返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法:
- 正常退出
- 异常退出
方法推出的过程实际上就等同于把当前栈帧出栈,因此退出是可能执行的操作有:
- 回复上层方法的局部变量表和操作数栈;
- 把返回值压入调用者栈帧的操作数占中;
- 调整PC计数器的值以指向方法调用指令后面的一条指令等;
附加信息
方法调用
方法调用不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,暂时还不涉及方法内部的具体运行过程。
解析
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在了加载的解析阶段,会将其中的一部分符号引用转化为直接引用。
静态方法、私有方法、实例构造器、父类方法、被final修饰的方法都适合在类加载阶段进行解析,将符号引用解析为该方法的直接引用,这些方法称为非虚方法。
解析调用一定是以静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。
分派
分派调用过程将会揭示多态性特征的一些最基本的体现。
1 静态分派
静态方法会在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的而过程也是通过静态分派完成的。
2 动态分派
它和多态性的另外一个重要体现—*重写有着很密切的关系。
3 单分派和多分派
方法的接受者与方法的参数统称为方法的宗量,单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
今天的Java语言是一门静态多分派、动态单分派的语言。
4 虚拟机动态分派的实现
使用虚方法表索引来代替元数据查找以提高性能。
方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。
动态类型语言支持(重要)
什么是动态类型语言:动态类型语言的关键特征是它的类型检查的主题过程是在运行期而不是编译期。
与运行时异常相对应的是连接时异常,例如很常见的NoClassDefFoundError便属于连接时异常。
基于栈的字节码解释执行引擎
解释执行
Javac编译器代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程,Java程序的编译就是半独立的实现。
基于栈的指令集与基于寄存器的指令集
Java编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流中的指令大比分都是零地址指令,它们依赖操作数栈进行工作。
基于栈的指令集主要的优点:
- 可移植;
- 代码相对更加紧凑
- 编译更加简单等
缺点:
- 完成相同功能需要的指令数量多。
- 频繁地访问内存,相对处理器来说,内存始终是执行速度额瓶颈;