天天看点

虚拟机字节码执行引擎

运行时栈帧结构

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

虚拟机字节码执行引擎

image.png

局部变量

局部变量表是一组变量值存储空间,用于存放方法参数和方法的内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。局部变量表的容量以变量槽Slot为最小单元,一个Slot一般是32位表示,能存储32为能表示的boolean、byte、char、short、int、float、reference或returnAddress类型的数据。long、double需要64位表示,占用两个slot。虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的Slot数量。如果是64位数据类型,需要使用两个连续的slot存储,不允许单独访问其中任何一个。

如果执行的是实例方法(非static方法),那局部变量表中第0个位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含的参数。局部变量表中的slot是可以重用的,方法体中定义的变量,其作用域不一定覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。如果变量对应的slot没有被其他变量使用前进行GC,这个变量不会被回收。

操作数栈

操作数栈又称为操作栈,是一个后入先出栈,存放操作指令和操作数。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。

动态连接

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

方法返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,蛰虫退出方式称为正常完成出口。另一种退出方式是在方法执行过程中遇到异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法异常表中没有找到匹配的异常处理,就会导致方法退出。这种退出方法称为异常完成出口。

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

方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定调用方法的版本,暂时还不涉及方法内部的具体运行过程。一切方法调用在Class文件里存储的都只是符号引用,而不是方法在实际运行时的内存地址,这个特性给java带来了更强大的动态扩展能力。

解析

在类加载过程中,解析这一步就是将限定符引用转成为直接内存引用。这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的(非接口、非重载的方法),主要包括静态方法和私有方法两大类。

方法调用的指令有下面5种:

1)invokestatic:调用静态方法

2)invokespecial:调用实例构造器<init>方法、私有方法和父类方法

3)invokevirtual:调用所有的虚方法

4)invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。

5)invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。上面4条调用指令,分派逻辑是固化在java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

invokestatic、invokespecial、部分invokevirtual(final方法调用)在解析阶段都是可以确定唯一版本的,称为非虚方法。其他指令在解析阶段是无法确定唯一版本的,涉及到分派逻辑。

分派

众所周知,Java是一门面向对象的程序语言,具有面向对象的3个基本特征:继承、封装和多态。分派调用过程是多态性特征实现的原理。

重载分派

重载方法的方法名相同,参数不同。虚拟机编译阶段在重载时是通过参数的定义类型而不是调用时传入的实际类型作为判断依据。Java虚拟机是通过方法定义时的类型来决定使用哪个重载版本的。所有依赖方法定义时的参数类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型场景就是方法重载。如果没有参数完全匹配的重载方法,则参数不断隐式转化成其父类,直到找到匹配的重载方法。

重写分派

重写方法是指在多个相关联的类中存在方法名相同,参数也相同的方法。在方法调用时,需要根据实例的类型确定调用哪个类中的方法。对应虚拟机内部实现是将类限定符解析为相应的类直接引用。这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分配。

**动态语言的支持

前面讲的分派都是基于invokevirtual指令实现的逻辑。在JDK7中迎来了新的方法调用指令invokedynamic,是为了支持动态语言(类型检查的主题过程是在运行期而不是编译期,“变量无类型,变量值才有类型”),也是JDK8可以实现Lamada表达式的基础。目前已经有很多动态语言运行在java虚拟机上,如Groovy、Jython、Rubby等。

**java.lang.invoke包

java7在JSR 292中增加了对动态类型语言的支持,使Java也可以像C语言那样将方法作为参数传递,其实现在lava.lang.invoke包中。MethodHandle作用类似于反射中的Method类,但它比Method类要更加灵活和轻量级。通过MethodHandle进行方法调用一般需要以下几步:

(1)创建MethodType对象,指定方法的签名;

(2)在MethodHandles.Lookup中查找类型为MethodType的MethodHandle;

(3)传入方法参数并调用MethodHandle.invoke或者MethodHandle.invokeExact方法。

这个过程其实就是模拟invokevirtual指令的执行过程,只不过它的分派逻辑并非固化在Class文件的字节码上,而是通过一个具体的方式来实现。

MethodType

可以通过MethodHandle类的type方法查看其类型,返回值是MethodType类的对象。也可以在得到MethodType对象之后,调用MethodHandle.asType(mt)方法适配得到MethodHandle对象。可以通过调用MethodType的静态方法创建MethodType实例,有三种创建方式:

(1)methodType及其重载方法:需要指定返回值类型以及0到多个参数;

(2)genericMethodType:需要指定参数的个数,类型都为Object;

(3)fromMethodDescriptorString:通过方法描述来创建。

创建好MethodType对象后,还可以对其进行修改,MethodType类中提供了一系列的修改方法,比如:changeParameterType、changeReturnType等。

Lookup

MethodHandle.Lookup相当于MethodHandle工厂类,通过findxxx方法(findStatics()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual、invokeinterface和invokespecial这几个字节码指令)可以得到相应的MethodHandle,还可以配合反射API创建MethodHandle,对应的方法有unreflect、unreflectSpecial等。

invoke

在得到MethodHandle后就可以进行方法调用了,有三种调用形式:

(1)invokeExact:调用此方法与直接调用底层方法一样,需要做到参数类型精确匹配;

(2)invoke:参数类型松散匹配,通过asType自动适配;

(3)invokeWithArguments:直接通过方法参数来调用。其实现是先通过genericMethodType方法得到MethodType,再通过MethodHandle的asType转换后得到一个新的MethodHandle,最后通过新MethodHandle的invokeExact方法来完成调用。

*invokedynamic指令

某种层度上,invokedynamic指令与MethodHandle机制的作用是一样的,都是为了解决原4条“invoke”指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户(包括其他动态语言的设计者)有更高的自由度。可以把它们想象成为了达到同一个目的,一个采用上层java代码和API来实现,另一个用字节码和Class中其他属性、常量来完成。

每一处含有invokedynamic指令的为止都称做“动态调用点”(Dynamic Call Site),这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK1.7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量可以得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型(MethodType)和名称。引导方法有固定的参数,并且返回值是java.lang.invoke.CallSite对象,这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法。

InvokeDynamic指定是为动态语言编译器提供,在java代码中没办法生成这条指令。