Java虚拟机第七章(虚拟机字节码执行引擎)

1.运行时栈帧结构

	栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine 
Stack)的栈元素.栈帧存储了方法的局部变量表,操作数栈,动态连接和方法的返回地址等信息.每一个方法从调用开始到执行完成的过程,就对应
着一个栈帧在虚拟机栈里面从入栈到出栈的过程.

	每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息.在编译程序代码的时候,栈帧中需要多大的局部变
量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影
响,而仅仅取决于具体的虚拟机实现.

	一个线程中方法调用链可能会很长,很多方法都同时处于执行状态.对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧
(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method).执行引擎所运行的所有字节码指令都只针对当前栈帧进行
操作.栈帧的概念结构如下图所示.

在这里插入图片描述

1.1 局部变量表

	局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量.在Java程序被编译为Class文件时,就在方法的Code属
性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量.

	局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只
是很有"导向性"地说明每个Slot都应该能存放一个boolean.byte,char,short,int.float,reference或returnAddress类型的数据,
这种描述与明确指出"每个Slot占用32位长度的内存空间"有一些差别,它允许Slot的长度随着处理器,操作系统或虚拟机的不同而发生变化.不
过无论如何,即使在64位虚拟机中使用了64位长度的内存空间来实现一个Slot,虚拟机仍要使用对齐和补白的手段让Slot在外观上看起来与32位
虚拟机中的一致.

	reference是对象的引用.虚拟机规范及没有说明它的长度,也没有明确指出这个引用应有怎样的结构,但是一般来说,虚拟机实现至少都应
当能从此引用中直接或间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据,而returnAddress是为字节码指令jsr,
jsr_w和ret服务的,它指向一条字节码指令的地址.

	局部变量表中的Slot是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超过了某个
变量的作用域,那么这个变量对应的Slot就可以交给其他变量使用.这样的设计不仅仅是为了节省栈空间,在某些情况下Slot的复用会直接影响到
系统的垃圾收集行为.

1.2操作数栈

	操作数栈也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈.同局部变量表一样,操作数栈的最大深度也在编译的时候
被写入到Code属性的max_stacks数据项之中.操作数栈的每一个元素可以是任意的Java数据类型,包括long和double.32位数据类型所占的
栈容量为1,64位数据类型所占的栈容量为2.在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值.

	当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就
是入栈出栈操作,例如:在做算数运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的.

1.3动态连接

	每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接,Class文件的常量池
中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数.这些符号引用一部分会在类加载阶段或第一次使用的
时候转化为直接引用,这种转化成为静态解析,另外一部分将在每一次的运行期间转化为直接引用,这部分成为动态连接.

1.4方法返回地址

	当一个方法被执行后,有两种方法退出这个方法.第一种方式值执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给
上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型讲根据遇到何种方法返回指令来决定,,这种退出方法的方式
称为正常完成出口(Normal Method Invocation Completion).

	另一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理.无论是Java虚拟机内部产生的异常,还是代码中
使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就回导致方法退出,这种退出方法的方式称为异常完
成出口(Abrupt Method Invocation Completion).一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的.

	无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,
用来帮助恢复它的上层方法的执行状态.一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器
值.而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息.

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

2.方法调用

	方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体执行
过程.

2.1解析

	所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引
用,这种解析能成立的前提是:方法在从程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的.换句话说,
调用目标在程序代码写好,编译器进行编译时就必须确定下来.这类方法的调用称为解析(Resolution).

	在Java里面,静态方法和私有方法适合在类加载阶段进行解析.

	与之相对应,在Java虚拟机里面提供了四条方法调用字节码指令,分别是:
		invokestatic:调用静态方法.
		invokespecial:调用实例构造器<init>方法,私有方法和父类方法.
		invokevirtual:调用所有的虚方法.
		invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象.
	
	只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法,私有方
法,实例构造器和父类方法四类,他们在类加载的时候就会把符号引用解析为该方法的直接引用,这些方法可以称为非虚方法,与之相反,其他方法
就称为虚方法(除去final方法)

2.2分派

	1.静态分派(主要作用于重载)
		所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派.静态分派的最典型应用就是方法重载.静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的.另外,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不
	是"唯一的".往往只能确定一个"更加合适的"版本.这种模糊的结论子在由0和1构成的计算机世界中算是个比较"稀罕"的事件,产生这种模
	糊结论的主要原因是字面量不需要定义,所以字面量没有显式的静态类型,它的静态类型只能通过语言上的规则去理解和推断.
	
	2.动态分派(主要作用于重写)
		invokevirtual(调用虚方法)指令的运行时解析过程大致分为以下步骤:
			1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C.
			2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查
		找过程将结束;不通过则返回java.lang.IllegalAccessError异常.
			3)否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程.
			4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常.
		由于invokevirtual指令执行的第一步就是在运行期确定接受者的实际类型,所以两次调用中的invokevirtual指令把常量池中的
	类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质.我们把这种在运行期根据实际类型确定方法执行版
	本的分派过程称为动态分派.
	
	3.单分派与多分派
		方法的接受者与方法的参数统称为方法的宗量,这个定义最早应该来源于<Java与模式>一书的译文.根据分派基于多少种宗量,可以将
	分派划分为单分派和多分派两种.单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个的宗量对目标方法进行选择.
		总结:今天的Java语言是一门静态多分派,动态单分派的语言.
		
	4.虚拟机动态分派的实现
		由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟
	机的实际实现中基于性能的考虑,大部门实现都不会真的进行如此频繁的搜索.面对这种情况,最常用的"稳定优化"有段就是为类在方法区
	中建立一个虚方法表(Virtual Method Table,也成为vtable,与此对应,在invokeinterface执行时也会用到接口方法表-
	Interface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能.
	
		虚方法表中存放着各个方法的实际入口地址.如果某个方法在子类中没有被重写,那么子类的虚方法表里面的地址入口和父类相同方法
	的地址入口是一致的,都指向父类的实现入口.如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址.
	
		虚拟机除了使用方法表之外,在条件允许的情况下,还会使用内联缓存(Inline Cache)和基于"类型继承关系分析"(Class 
	Hierarchy Analysis,CHA)技术的守护内联(Guarded Inlining)两种非稳定的"激进优化"手段来获得更高的性能.

3.基于栈的字节码解释执行引擎

	3.1解释执行
		Java语言中,javac编译器完成了程序代码经过词法分析,语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程.因为
	这一部分动作实在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以Java程序的编译就是半独立的实现.
	
	3.2基于栈的指令集与基于寄存器的指令集
		Java编译器输出的指令流,基本上是一种基于栈的指令集架构(Instruction Set Architecture,ISA),指令流里面的指令大部分
	都是零地址指令,它们依赖操作数栈进行工作.与之相对的另外一套常用的指令集架构师基于寄存器的指令集,最典型的就是X86的二地址指
	令集,更通俗一些,就是现在我们主流PC中直接支持的指令集架构,这些指令依赖寄存器进行工作
	
		基于栈的指令集最主要的优点就是可移植性,以及代码相对更紧凑,编译器实现更简单等.缺点是执行速度相对来说稍慢一些.
	原因是指令数量和内存访问,是导致栈架构指令集的执行速度相对较慢
	
	3.3基于栈的解释器执行过程

猜你喜欢

转载自blog.csdn.net/qq_42363892/article/details/86472433
今日推荐