JVM: 字节码执行引擎(1)-概述

执行引擎是JVM最核心的组成部分之一。

虚拟机执行引擎在执行代码的时候,可能会有解释执行(通过解释器执行)和编译执行(通过JIT编译器产生本地代码执行)两种选择。

虚拟机是一个相对于物理机的概念,它们都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎则需要自己实现。

运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。

栈帧存储了方法的局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息(如与调试相关的信息)等。

每一个方法从调用从开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

局部变量表

局部变量表用于存放方法参数和方法内部定义的局部变量,局部变量表的容量以变量槽(Slot)为最小单位。编译的时候,方法的Code属性的max_locals数据项中就确定了该方法所需分配的局部变量表的最大容量。

一个Slot可以存放一个32位以内的数据类型,Java中占用32位以内的数据类型有:boolean、byte、char、short、int、float、reference和returnAddress 8种类型。

  • reference类型:表示一个对象实例的引用。虚拟机可以从此引用直接或间接地查找到对象在Java堆中的数据存放的起始地址索引;还可以从此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息。
  • returnAddress类型:目前已经很少见了,它是为字节码指令jsrjsr_wret服务的,指向了一条字节码指令的地址。

对于64位的数据类型,虚拟机以高位对齐的方式来分配两个连续的Slot空间存储。

局部变量表是建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否为原子操作,都不会引起数据安全问题。

不使用对象手动赋值为null,help GC?
局部变量表中的Slot是可以重复使用的,如果一个变量在没有任何读写操作后,就可以将该变量占用的Slot进行重复使用。
但是如果没有任何对局部变量表的读写操作,不需要使用的变量占用的Slot还没有被其他变量复用。这是可以使用手动赋值null来帮助回收。

问题:在虚拟机使用解释执行时,使用null值得操作来优化内存回收是有用的;但是经过JIT编译优化后就会把赋null值优化掉,这是将变量设置为null值就是没有意义的。

操作数栈

操作数栈(Operand Stack)也称为操作栈,是一个先入后出(LIFO)的栈。操作栈的最大深度也是在编译的时候写入到Code属性的max_stacks数据项中。

动态连接

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

字节码中的方法调用指令是以常量池中指向方法的符号引用作为参数。

  • 这些符号引用一部分在类加载阶段或初始化时就转化为直接引用,这种转化称为静态解析
  • 另一部分在每次运行期间转化为直接引用,称为动态连接

方法返回地址

当一个方法开始执行时,只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令。另一种方式是方法执行时遇到了异常,并且这个异常没有在方法体内得到处理。

无论方法采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。

方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。

方法正常退出时,调用者的PC计数器的值作为返回地址;方法异常退出时,返回地址是通过异常处理器来确定的。栈帧中一般不会保存这个信息。

基于栈和基于寄存器解释执行比较

基于栈的解释执行主要优点是:可移植,平台无关性。寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地受到硬件的约束。

基于栈的解释执行主要缺点是:执行速度相对来说会慢一点,主要是指令数量和内存访问的所导致的。