Java虚拟机系列之字节码执行引擎

406 阅读5分钟

栈帧结构

其实在前面的文章也提到过栈帧,栈帧是随着方法调用时,压入栈的数据结构,对于执行引擎来说,只有栈顶的栈帧才是有效。一个栈帧包括了局部变量表、操作数栈、动态连接、方法返回地址等等

局部变量表

局部变量表以slot为单位,每个slot可以是占用32位长度的内存空间,用来存放boolean、byte、char、short、int、float、reference或returnAddress类型的数据,对于64位的数据只有long和double,会占用两个slot,而reference并没有规定的长度。

为了节省栈帧空间,slot是可以重用的,但重用可能会有额外的副作用。

操作数栈

操作数栈就是为了给字节码指令提供操作数,一开始为空,随着各种指令往操作数栈中写或度数据。操作数栈的每一个元素,可以是任意的Java数据类型,包括long和double,但操作数栈的深度不会超过max_stacks数据项中设定的最大值。

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在《Java虚拟机系列之类文件结构》中讲到,常量池持有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分在类加载或第一次使用时就转化为直接引用,成为静态解析。另一部分在每一次运行期间转化为直接引用,这部分成为动态连接。

方法返回地址

方法返回地址,就是方法返回后继续执行的字节码指令地址。

方法调用

方法调用不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪个方法),还未涉及方法内部的指令执行。

解析

在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析的前提是:方法在真正运行之前就有一个可确定的调用版本,并且在运行期是不可改变的。这类方法的调用成为解析。

这类方法包括:静态方法、私有方法、实例构造器和父类方法

分派

解析调用时一个静态过程,在编译期间就已经确定了,而分派调用则可能是静态,也可能是动态的

静态分派

静态分派应用于方法重载,发生在编译阶段,如

public class StaticDispatch{

	static abstract class Human{}
	
	static class Man extends Human{}
	
	static class Woman extends Human{}
	
	public void sayHello(Human guy){
		System.out.println("hello,guy");
	}
	
	public void sayHello(Man guy){
		System.out.println("hello,gentleman");
	}
	
	public void sayHello(Woman guy){
		System.out.println("hello,llady");
	}
	
	public static void main(String[] args) {
		Human man = new Man();
		Human woman = new Woman();
		StaticDispatch sr = new StaticDispatch();
		sr.sayHello(man);
		sr.sayHello(woman);
	}

}

输出结果:

hello,guy! hello,guy!

在main方法中,Human 成为变量的静态类型,Man和Woman成为变量的实际类型。方法接收者已经确定是对象sr前提下,使用哪个重载版本,就完全取决于传入参数的数量的数据类型,在编译阶段,静态类型虚拟机是知道的,所以使用静态类型调用方法。

动态分派

动态分派跟多态性的重写有关


public class StaticDispatch{

	static abstract class Human{
		protected abstact void sayHello();
	}
	
	static class Man extends Human{
		@Override
		protected void sayHello() {
			System.out.println("man says hello");
		}
	}
	
	static class Woman extends Human{
		@Override
		protected void sayHello() {
			System.out.println("woman says hello");
		}
	}
	
	
	public static void main(String[] args) {
		Human man = new Man();
		Human woman = new Woman();
		man.sayHello();
		woman.sayHello();
		man = new Woman();
		man.sayHello(woman);
	}

}

结果输出:

man says hello
woman says hello
woman says hello

这个结果并不意外,根据结果可以得知,虚拟机是根据实际类型来分派方法执行的版本的。

那动态分派的实现又是怎样的呢? 是使用虚方法表实现的,具体先留着,等后面有时间再补上......

字节码解析执行

前面讲解了虚拟机是如何调用方法的,这里讨论虚拟机是如何执行方法中的字节码指令的

解析执行

许多Java虚拟机的执行引擎在执行Java代码时有解析执行(通过解析器执行)和编译器执行(通过即时编译器产生本地代码执行)两种选择。大部分的程序代码到物理机的目标代码或虚拟机能执行的指令集之前,都需要经过下图的各个步骤。

像C/C++语言来说,词法分析、语法分析和后面的优化器和生成器都可以独立于执行引擎。而像Java语言来说,可以选择把其中的一部分(到抽象语法树)实现为一个半独立的编译器。Java编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。

基于栈的指令集和基于寄存器的指令集

所谓基于栈和基于寄存器的意思是,指令的操作对象是栈或者寄存器。基于栈的指令集表现形式为:

iconst_1
iadd
istore_0

基于寄存器的指令集表现形式为:

mov eax, 1
add eax, 1

基于栈的指令集只是虚拟机为了可移植而实现的指令集,而虚拟机是逃不掉基于寄存器的指令集的,所以基于栈的指令集的实现,最底层还是依靠基于寄存器的指令集实现的。这没人反对吧......