登录/注册
小码仔
799
占位
0
占位
2
浏览量
占位
粉丝
占位
关注
深入了解jvm-2Edition-虚拟机字节码执行引擎
小码仔
2021-10-13 09:18:42 2021-10-13
14
0

1、概述

Java虚拟机规范制定了虚拟机字节码执行引擎的概念模型,本章主要从概念模型层次来探究虚拟机的方法调用和字节码执行。

方法调用中,最核心的,是如何确定调用的方法,也就是方法的分派

字节码执行过程中,特别重要的一点是执行上下文的切换和信息的交换处理。这需要运行时数据结构的支持,也就是运行时栈帧

2、运行时栈帧结构

运行时栈帧(Stack Frame)是用于支持虚拟机方法调用和方法执行的数据结构。

它是虚拟机运行时数据区中的虚拟机栈的栈元素。

存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。

方法的调用、执行、返回过程就是栈帧在栈里入栈(创建)、内部信息改变、出栈(销毁)的过程。

在编译过程中,栈帧中的局部变量表的大小、操作数栈的深度就已经确定并记录在了方法的code属性里面了。

对于执行引擎来说,只有栈顶的栈帧(当前栈帧,对应当前方法)是有效的。

1、局部变量表

存放方法参数和方法内部定义的局部变量

容量以槽(Slot)为最小单位。

虚拟机规范没有规定槽的大小,

只说了每个槽都能存放一个boolean、byte、char、short、int、float、reference、或

returnAddress 数据类型。

因此可以说一个Slot可以存放一个32位及以下的数据类型。

64位的数据类型要占用两个Slot(long、double),高位对齐

reference数据至少要能帮助虚拟机完成两项功能:

1、直接或间接地查找到对象在Java堆中的起始地址;

2、直接或间接地在方法区中查找到对象所属数据类型(对象的元数据)。

局部变量列表中,索引从0开始,第0位存放的是方法隐含的参数this(非static方法)

其余位置先按参数列表的顺序存放参数,再按局部变量定义的顺序存放局部变量。

局部变量表中的引用会影响到GC的行为,因为它是GC Roots之一。

如果局部变量表中的引用还存在,那么GC就不会清除引用指向的对象。

将对象引用置为null来帮助GC的原理就是手动将局部变量表中对应的的Slot清空。

置null操作意义不大,这通常会被编译器优化掉。。。

最重要的一点!局部变量表不像方法区中的类一样有初始化赋值过程(准备阶段)

因此,没有赋初始值的局部变量是不能使用的。不像类变量一样有系统初始值。

2、操作数栈

操作数栈是方法执行的最基础的支撑。

操作数栈中元素的数据类型要与字节码指令严格匹配,这在编译时会保证,在类校验阶段还要再次验证。

3、动态链接

指向方法区中运行时常量池中该栈帧所属方法的引用,为了支持方法调用过程中的动态链接。

静态解析:在类加载或第一次使用的时候就将符号引用转换为直接引用。

动态链接:在运行期间才转转为直接引用。

4、方法返回地址

正常完成出口:方法正常执行退出

异常完成出口:。。。

方法退出过程就是将当前栈帧出栈,并恢复上层方法的局部变量表和操作数栈

返回值压入上层方法的操作数栈中,调整PC的值,指向下一条指令。

5、附加信息

调试信息等。

3、方法调用

方法调用不等同于执行,调用只是确定是哪一个方法(参数、返回值、所属类)。

1、解析

调用目标在编译期就确定,这就是解析调用。

方法能解析的前提:方法在程序运行前就有一个可确定的调用版本,并且该版本在运行期不变。

符合该前提的方法主要包括静态方法和私有方法

静态方法直接和类关联,私有方法不可访问,因此它们都不可通过继承或其他方式重写。

虚拟机中的方法调用指令:

1、invokespecial:调用构造器,私有方法和父类方法。

2、invokestatic:调用静态方法。

3、invokevritual:调用虚方法

4、invokeinterface:调用接口方法

5、invokedynamic:动态解析调用方法。

  只要能够被1、2调用的方法都可以在解析时确定。

4、方法调用-分派

解析调用在编译期完成,是静态的。

分派则可以是静态的也可以是动态的。

按照宗量数又可分为单分派多分派。(方法接收者与参数统称为方法宗量)

因此,就可组合出:动/静态单/多分派 四种分派方式。

静态分派是重载的虚拟机层面的实现。动态分派是重写的虚拟机层面的实现。

    

1、静态分派

Human man = new Man();

其中,

Human 称为变量的静态类型(Apparent Type)

Man 称为变更量的实际类型(Actual Type)

静态类型在编译时就可以确定,但是实际类型要在运行时才能确定。

其实,从英文名就很好理解,Apparent Type就是表面上的类型,Actual Type就是实际上的类型。

对于

man ,在编译时就可以确定它是一个

Human 类型,但是,他到底是

Man 还是

Woman 要等程序运行时才知道。

方法被重载时,是通过静态类型作为方法的选择依据的,因此在编译时就可以选定重载方法。

依据静态类型来定位方法的执行版本的分派就称为静态分派。

所以,静态分派不是虚拟机做的,它是编译期做的。

2、动态分派

   既然静态分派是在编译期,那么动态分派就在运行期咯。

void sayHello(Human human){ human.hello(); }

sayHello(man);

sayHello(woman);

对于上述代码,怎么去确定

human.hello() 要调用的方法呢?

javap 反编译后,发现它们都是由

invokevirtual 调用的,但是,两个

invokevirtual 都是指向的

Human 的

hello() 。

但是两个执行的方法明显是不同的。

这就是因为

invokevirtual 指令的多态查找过程:

1、找到操作数栈栈顶的元素指向的对象的实际类型,记为C。

都找到实际类型了,多态不就解决了。

2、在C中查找与

invokevirtual 指令参数常量描述符简单名都相符的方法,

找到后,要检查访问权限,权限不通过,则抛出

IllegalAccessError 异常。

3、否则,到继承链上寻找。

4、否则,抛出

AbstractMethodError 异常。

可以看出,

invokevirtual 指令的执行结果是和操作数栈的状态相关的,

还可以看出,调用对象方法时,首先要做的,就是将对象引用入栈。

因此就多态就实现了。

3、单分派和多分派

方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少宗量,可以将分派划分为单分派和多分派。

上面代码中,对

father.Chioce(new Candy()); 处代码 编译期选择依据两点:

注意

father 的类型是可编译时确定的。因此为静态分派

1、静态类型是

Father 还是

Son ;

2、方法参数是

Candy 还是

Fist 。

基于两个宗量进行的,因此静态分派属于多分派类型

son.Choice(new Candy()) ; 处调用:

 

son 的类型在编译期无法确定,因此为

动态分派 。

  但是,此时编译器已经指定了方法的参数必须是

Candy 类型的。

  因此,动态分派时只需要确定方法的所属类。

  因此,Java的动态分派属于单分派类型

Java是静态多分派,动态单分派的类型

4、虚拟机动态分派实现

出于性能考虑,在实现中,为类在方法区中建立了一个虚方法表(Virtual Method Table)

用于

invokevirtual指令 执行时,直接在该虚方法表中查找方法。

虚方法表中存放着各个方法的实际入口地址

如果子类没有重写父类方法,那么子类的虚方法表中,该方法指向父类方法的实现入口。 

如果子类重写了,就指向子类自己的实现的入口。

为了实现方便,相同签名的方法在子类和父类虚方法表中的索引都一样。

虚方法表一般在类加载的链接阶段初始化,就是在类第一次初始化之后。

为了

invokeinterface 执行,也建立了接口方法表(Interface Method Table)

5、动态类型语言支持

动态类型语言可以实现在运行时自由地为类绑定字段和方法,这就要求,在进行方法分派时,可以有自己的选择。

但是目前讲到的分派,方法分派时的查找都是规定好了的。

因此,要支持动态类型支持,就要将方法分派的接口分享出来,让我们可以自己去进行分派。

jdk1.7引入了

java.lang.invoke 包,提供了一种新的动态确定目标方法的机制:

MethodHandle

A method handle is a typed, directly executable reference to an underlying method, constructor, field,

or similar low-level operation, with optional transformations of arguments or return values.

也就是说,除了只能把类作为单独实体来使用,我们可以通过MethodHandle将方法也抽象成一个单独实体。

(虽然也是通过类来实现的。。。)

好了,我们现在能单独使用方法了,但是,还得找到它吧。

这就涉及到怎么确定一个方法

1、方法所属类

2、方法简单名

3、方法描述符(参数,返回值)

MethodType

A method type represents the arguments and return type accepted and returned by a method handle,

or the arguments and return type passed and expected by a method handle caller. 

MethodType封装了对方法描述符的表示。

现在:

1、类可以用类的Class对象表示;

2、方法简单名——字符串

3、方法描述符——MethodType

就可以去找方法了。

MethodHandles类为我们提供了许多根据上述标识找方法的封装。太贴心了。

invokedynamic指令:

同MethodHandle机制一样,只是MethodHandle是上层实现,invokedynamic是底层实现

每一处

invokedynamic 指令的位置都被称作动态调用点(Dynamic Call Site)

CallSite:

A CallSite is a holder for a variable MethodHandle, which is called its target.

An invokedynamic instruction linked to a CallSite delegates all calls to the site's current target.

invokedynamic指令的第一个参数不是

CONSTANT_Methodref_info 常量,

而是新增的

CONSTANT_InvokeDynamic_info 。

CONSTANT_InvokeDynamic_info包含三个信息:

1、引导方法

2、方法类型MethodType

3、方法名称

根据前面分析,方法名称、描述符有了,但是还差方法所属类。所以,引导方法中,应该要提供查找类!

引导方法(Bootstrap Method):

存放在

BootstrapMethods 属性中,是有固定参数,并且返回值是

java.lang.invoke.CallSite 对象的方法。

代表真正要执行的目标方法调用。

根据CONSTANT_InvokeDynamic_info中的信息,虚拟机找到并执行引导方法,得到一个CallSite对象,

最终使用CallSite调用目标方法。

现在有了方法的标识,谁去帮我们找呢?

MethodHandles.Lookup lookup() :

Returns a Lookup object with full capabilities to emulate all supported bytecode behaviors of the caller.

Lookup对象可以模拟调用的字节码行为。就是它了。

   

6、 基于栈的字节码解释执行引擎

主要注意,基于操作数栈,数据交换都要经过操作数栈。指令也是针对栈元素进行操作的。

 

 

 

 

 

 

 

原文: https://www.cnblogs.com/lqblala/p/15144165.html

暂无评论