虚拟机执行引擎

执行引擎是java虚拟机最核心的组成部分之一。虚拟机是相对于“物理机”的概念,这两种机器都有代码执行能力,区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎则是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。

在java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观,在不同的虚拟机实现里面,执行引擎在执行java代码的时候可能有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择。也可能两者兼备,甚至可能包含几个不同级别的编译器执行引擎,但从外观看来,所有的java虚拟机的执行引擎都是一致的:输入的是字解码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

1、运行时栈帧结构

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

在编译程序代码的时候,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入到方法表的Cose属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

一个线程中方法调用链可能会很长,很多方法都同时处于执行状态,对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。那么栈帧中的局部变量表、操作数栈、动态连接、方法返回地址等各个部分的作用和数据结构是什么呢?

(1)局部变量表

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


局部变量表的容量以变量槽(Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是说明每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。reference是对象的引用,虚拟机规范没有说明它的长度,也没有明确指出这个引用应该有怎样的结构,但一般来说,虚拟机实现至少都应当能从此引用中直接或间接地查找到对象在java堆中的起始地址索引和方法区中的对象类型数据,而returnAddress是字节码指令jsr、jsr_w和ret服务。它指向了一条字节码指令的地址。

对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的slot空间。java语言中明确规定64位的数据类型只有long和double两种(reference可能是32位也可能是64位)。这里吧long和double数据类型分割存储的做法和“long和double的非原子性协定”中把一次long和double数据类型读写分割为两次32位读写的做法类似。不过由于局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否是原子操作,都不会引起数据安全问题。

虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的Slot数量,如果是32位数据类型的变量,索引n就代表了使用第n个Slot,如果是64位的,则说明要使用第n和第n+1连个Slot。

在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static方法),那么局部变量表中的第0位索引的slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含的参数,其余参数则按照参数表的顺序来排列,占用从1开始的局部变量slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。

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

(2)操作数栈

操作数栈也常常成为操作栈,它是一个后入先出栈,同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到code属性的max_stack数据项中,操作数栈的每一个元素可以是任意的java数据类型,包括long和double。32位数据类型所占用的栈容量为1,64位数据类型所占的栈容量是2,在方法执行的人任何时候,操作数栈的深度都不会超过max_stack数据项设定的最大值。

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

在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的。但是大多数虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠,让下面栈帧的部分操作数栈与与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,而无须进行额外的参数复制传递了。


java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中的栈就是操作数栈。

(3)动态连接

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

(4)方法返回地址

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

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

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

2、方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。程序运行时,进行方法调用是最普遍、最频繁的操作,Class文件的编译过程不包含传统编译总的连接步骤,一切方法调用在class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址,这个特性给java带来了更强大的动态扩展能力,但也使得java方法的调用过程变得相对复杂,需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。

(1)解析

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

在java语言中,符号“编译期可知,运行期不可变”这个要求的方法主要由静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法都不可能通过继承或者别的方式重写出其他版本,因此它们都适合在类加载阶段进行解析。

与之相对应:在java虚拟机里面提供了四条方法调用字节码指令,分别是:

invokestatic:调用静态方法

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

invokevirtual:调用所有的虚方法

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

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器和父类方法四类。他们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法,与之相反,其他方法就称为虚方法(除去final方法)。

java中的非虚方法除了使用invokestatic和invokespecial调用的方法之外还有一种,就是被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的,但是由于它无法覆盖,没有其他版本,所以也无须对方法接收者进行多态选择,又或者说多态选择的结果肯定是唯一的。在java语言规范中明确说明了final方法是一种非虚方法。

(2)分派

java是一门面向对象的语言,因为java具备面向对象的三个基本特征:继承、封装、多态,分派调用过程揭示了多态性特征的一些基本体现(重载和重写),那么虚拟机是如何确定正确目标方法呢?

a、静态分派

public class StaticDispatch {
    static abstract class Human {

    }
    static class Man extends Human{

    }
    static class Women 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(Women guy) {
        System.out.println("hello, lady!");
    }
    public static void main(String[] args) {
        Human man = new Man();
        Human women = new Women();
        StaticDispatch sd = new StaticDispatch();
        sd.sayHello(man);
        sd.sayHello(women);
    }
}
输出结果:

hello, guy!
hello, guy!

Human man = new Man();

其中Human称为变量的静态类型(static Type)或者外观类型,后面的Man称为变量的实际类型,静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会改变, 并且最终的静态类型是在编译期可知的,而实际类型变化的结果在运行期才可以确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

实际类型变化:

Human man = new Man();

man = new Women();

//静态类型变化

sr.sayHello((Man) man);

sr.sayHello((Women) man);

上面代码main()里面的两次sayHello()方法调用,在方法接收者已经确定是对象“sr”的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型,代码中刻意定义了两个静态类型相同,实际类型不同的变量,但虚拟机(准确来说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的,并且静态类型是编译器可知的,所以在编译阶段,javac编译器就根据参数的静态类型决定使用哪个重载版本,所以选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。

所有依赖静态类型定位方法执行版本的分派动作,都称为静态分派。静态分派最典型应用就是方法重载,静态分派发在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但是这个重载版本并不是“唯一的”,往往只能确定一个“更加合适的”版本。

b、动态分派

动态分派和多态性的另一个重要体现-重写有着密切联系。

public class DynamicDispatch {
    static abstract class Human{
        protected abstract void sayHello();
    }
    static class Man extends Human{
        @Override
        protected void sayHello() {
            System.out.println("man say hello");
        }
    }
    static class Women extends Human{
        @Override
        protected void sayHello() {
            System.out.println("women say hello");
        }
    }
    public static void main(String[] args) {
        Human man = new Man();
        Human women = new Women();
        man.sayHello();
        women.sayHello();
        man = new Women();
        man.sayHello();
    }
}

man say hello
women say hello
women say hello


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值