虚拟机字节码执行引擎1

编译器讲源代码转成字节码,那么字节码如何被执行的呢?这就涉及到了JVM的字节码执行引擎,执行引擎负责具体的代码调用及执行过程。就目前而言,所有的执行引擎的基本一致:输入:字节码文件处理:字节码解析输出:执行结果。

不同的虚拟机实现里面,执行引擎在执行Java代码时可能有解释执行编译执行两种选择。物理机的执行引擎是由硬件实现的,和物理机的执行过程不同的是虚拟机的执行引擎由于自己实现的。

一、运行时候的栈结构

每一个线程都有一个虚拟机栈,栈中的基本元素我们称之为栈帧。栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。每个栈帧都包括:局部变量表、操作数栈、动态连接、方法的返回地址和一些额外的附加信息每一个方法从调用开始到执行结束都对应一个栈帧的入栈到出栈过程。

栈帧中需要多大的局部变量表和多深的操作数栈在编译过程中已经完全确定,并写入到方法表的Code属性(字节码的最终指令存储在code属性中,接口和抽象类没有code属性在活动的线程中,位于当前栈顶的栈帧才是有效的,称之为当前帧,与这个栈帧相关联的方法称为当前方法执行引擎运行的所有字节码指令只针对当前栈帧进行操作。需要注意的是一个栈中能容纳的栈帧是受限,过深的方法调用可能会导致StackOverFlowError,

1、局部变量表

方法参数方法内部定义的局部变量组成,其容量用Slot作为最小单位。在编译期间,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程

Slot的长度可以随着处理器操作系统虚拟机的不同而发生变化,但1个slot可以放下32位长度的空间(比如float int reference char),对于64位的longdouble会以高位对齐的方式分配两个slot空间。局部变量表是线程私有 无论两个连续的slot是否是原子操作,都不会引起线程安全问题。

如果是实例方法,那局部变量表第0位索引的Slot存储的是方法所属对象实例的引用,因此在方法内可以通过关键字this来访问到这个隐含的参数。其余的参数按照参数表顺序排列,参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。举例说明:

public void test(){

    call(2,3);

    ...

    call2(2,3);

}

public void call(int i,int j){

    int b=2;

      ...

}

public static void call2(int i,int j){

    int b=2;

    ...

}

假设以上两段代码在同一个类中。这时call()所对应的栈帧中的局部变量表大体如下: 实例方法中有个隐含的this指针变量
 
而call2()所对应的栈帧的局部变量表大体如下: 

2、操作数栈

和局部变量类似,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。操作数栈中的每一个元素可以是Java任意类型,32位数据占1个栈容量,64位栈两个当方法刚开始执行时,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数中写入和提取内容,也就是出栈/入栈操作。操作数栈中元素的数据类型必须与字节码指令的序列严格匹配2,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。在概念模型中两个栈帧作为虚拟机栈是相互独立的,但是多数的虚拟机实现里会有优化处理,两个栈帧中有共享的操作数栈。另外我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈

3、动态连接

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

4、方法返回地址

方法执行后两种方式退出:遇到返回指令 正常退出;发生异常区别在于:通过异常退出的不会给他的上层调用者产生任何的返回值。 
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置,方法正常退出时,调用者的pc计数器的值作为返回地址,而通过异常退出的,返回地址是要通过异常处理器

二、方法调用

方法调用的主要任务就是确定被调用方法的版本(即调用哪一个方法,不是方法执行),该过程不涉及方法具体的运行过程。按照调用方式共分为两类:

1. 解析调用是静态的过程,在编译期间就完全确定目标方法。

2. 分派调用即可能是静态,也可能是动态的,根据分派标准可以分为单分派和多分派。两两组合有形成了静态单分派、静态多分派、动态单分派、动态多分派

解析

在Class文件中,所有方法调用中的目标方法都是常量池中的符号引用,在类加载的解析阶段,会将一部分符号引用转为直接引用,也就是在编译阶段就能够确定唯一的目标方法,这类方法的调用成为解析调用(静态的)。此类方法主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可访问,因此决定了他们都不可能通过继承或者别的方式重写该方法,符合这两类的方法主要有以下几种:静态方法、私有方法、实例构造器、父类方法。虚拟机中提供了以下几条方法调用指令:

1. invokestatic:调用静态方法,解析阶段确定唯一方法版本

2. invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本

3. invokevirtual:调用所有虚方法

4. invokeinterface:调用接口方法

5. invokedynamic:动态解析出需要调用的方法,然后执行

前四条指令固化在虚拟机内部,方法的调用执行不可认为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外[^footnote4])称为虚方法。final虽然是invokevirtual调用的,但是因为final方法不允许被重写,所以他也是非虚方法。

       非虚方法(invokestatic指令和invokespecial指定调用的和final方法。静态方法、私有方法、父类方法、final方法)在编译阶段就可唯一确定,所以解析时直接将符号引用转为直接引用。是静态的。

分派

分派调用更多的体现在多态上。

1. 静态分派:所有依静态类型来定位方法执行版本的分派成为静态分派,发生在编译阶段,典型应用是方法重载

2. 动态分派:在运行期间根据实际类型来确定方法执行版本的分派成为动态分派,发生在程序运行期间,典型的应用是方法的重写

3. 单分派:根据一个宗量对目标方法进行选择。

4. 多分派:根据多于一个宗量对目标方法进行选择。

特别重要:                                 

public class Run {
	//静态分派
	static abstract class Human{
		
	}
	static class Man extends Human{
		
	}
	static class Woman extends Human{
		
	}
	
	//方法
	public void sayHello(Human guy){
		System.out.println("guy");
	}
	public void sayHello(Man man){
		System.out.println("man");
	}
	public void sayHello(Woman woman){
		System.out.println("woman");
	}
	

	public static void main(String[] args){
		Run run=new Run();
		Human man=new Man();//Human为静态类型 Man为实例类型,二者属于静态分派 所以man实际上还是Human类型 静态类型在编译期可知
		Human woman=new Woman();
		run.sayHello(man);//结果为guy
		run.sayHello(woman);//结果为guy
		//执行结果为 guy guy

	}
}

public class Run {
	//动态分派
		abstract class Human2{
			public abstract void sayHello();

		}
		class Man2 extends Human2{
			public void sayHello(){
				System.out.println("man2");
			}
		}
		class Woman2 extends Human2{
			public void sayHello(){
				System.out.println("woman2");
			}
			
		}
		public static void main(String[] args){
			Run run=new Run();
		
			Human2 man2=new Man2();
			Human2 woman2=new Woman2();
			man2.sayHello();
			woman2.sayHello();
			//执行结果为man2  woman2
		}
}

invokevirtual指令的运行时解析过程:

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

由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派

虚拟机动态分派的实现

由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据搜索合适的目标方法,因此虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正的进行如此频繁的搜索。面对这种情况,最常用的稳定优化手段就是为类在方法区中建立一个虚方法表Virtual Method Table,也称为vtable),使用虚方法表索引来代替元数据查找以提高性能。


虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都是指向父类的实际入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实际版本的入口地址。

为了程序实现上的方便,具有相同签名的方法,在父类、子类的虚方法表中具有一样的索引序号,这样当类型变换时,仅仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需要的入口地址。

方法表一般在类加载阶段连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值