虚拟机字节码执行引擎

一、概述

物理机的执行引擎:直接建立在处理器、硬件、指令集和操作系统层面

虚拟机的执行引擎:由自己实现,可以自行制定指令集与执行引擎的结构体系,并且能够执行不被硬件直接支持的指令集格式。

java虚拟机的执行引擎:输入字节码文件,处理过程是字节码解析的等效过程,输出是执行结果。

二、运行时栈帧结构

栈帧:用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素。

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每个方法从调用开始至执行完成的过程都对应一个栈帧
在虚拟机栈里从入栈到出栈的过程。

在编译程序代码时,栈帧需要多大的局部变量表、多深的操作数栈都已经完全确定,并写入方法表Code属性中,因此一个栈帧需要分配多少内存,
不会受程序运行期数据的影响。

对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(current stack frame),与这个 栈帧相关联的方法
称为当前方法(current method)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

1、局部变量表

局部变量表(local variable table) 是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

在java程序编译为Class文件时,在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。

局部变量表的容量以变量槽(Variable Slot,简称Slot)为最小单位,每个变量槽都能存放一个32位以内的数据类型由
boolean、byte、char、short、int、float、reference、returnAddress类型数据;64位的数据类型虚拟机以高位对齐的方式
为其分配两个连续的Slot空间,如double、long。

局部变量表通过索引定位的方式使用局部变量表,索引范围是0~局部变量表最大Slot数量;
第0位用于传递方法所属对象实例的引用,可以通过关键字“this”来访问到这个隐含参数。

类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;一次在初始化阶段,赋予程序员定于的初始值。
局部变量定义后没有赋值是不能使用的。

2、操作数栈

操作数栈(operand stack ,操作栈),后入先出(last in first out)的栈,最大深度在编译时写入Code属性的max_stacks数据项中。
操作栈的每一个元素可以是任意的java数据类型,包括long、double,32为数据类型占栈容量1,64位占2;方法执行时,操作数栈的
深度不会超过在max_stacks数据项中设定的最大值。

当一个方法刚刚开始执行时,方法的操作数栈为空,在方法执行过程中,字节码指令向操作数栈写入和提取内容,也就是出栈、入栈操作。

3、动态链接

每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,这个引用为了支持调用过程中动态链接(Dynamic linking)

4、方法返回地址

退出方法:

  • 正常完成出口:执行引擎遇到任意一个方法返回的字节码指令
  • 异常完成出口:在执行方法过程中遇到了异常,并且这个异常没有在方法体内处理。

三、方法调用

方法调用不等同与方法执行,方法调用阶段确定调用哪一个方法,不涉及方法内部具体运行过程。

1、解析

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

Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法;静态方法与类型直接关联,私有方法在外部
不可访问,这两种方法各自特点决定了他们都不可能通过继承和重写来确定其他版本,因此都在类加载阶段进行解析。
方法调用字节码指令:

  • invokestatic:调用静态方法
  • invokespecial:调用实例构造器<init>方法、私有方法、父类方法
  • invokevirtual:调用所有虚方法
  • invokeinterface:调用接口方法、在运行时在确定一个实现此接口的方法
  • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后在执行该方法,在此之前的4条调用指令,分派逻辑是固话
    在java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

只要能被invokestatic、invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,
包括静态方法、私有方法、实例构造器、父类方法,在类加载时,会把所有的符号引用解析为该方法的直接调用。这些方法称为非虚方法。
非虚方法还包括:final方法,虽然被invokevirtual方法调用,,但是其无法被覆盖,没有其他版本,也就没有多态选择或者说多态选择唯一。

解析调用是一个静态调用的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用替换为可确定的直接引用,不会延迟
到运行期完成。

2、分派

2.1 静态分派(Method Overload Resolution)
    public class StaticDispatch {
        static abstract class Human {
    
        }
    
        static class Man extends Human {
    
        }
    
        static class Woman extends Human {
    
        }
    
        public void sayHello(Human human) {
            System.out.println("hello guy!");
        }
    
        public void sayHello(Man guy) {
            System.out.println("hello,gentleman!");
        }
    
        public void sayHello(Woman woman) {
            System.out.println("hello,lady!");
        }
    
        public static void main(String[] args) {
            Human man = new Man();
            Human woman = new Woman();
            StaticDispatch sr = new StaticDispatch();
            sr.sayHello(man);
            sr.sayHello(woman);
        }
    }
    //输出结果:
    hello guy!
    hello guy!

    

定义两个重要概念:

    Human man = new Man();

“Human”称为静态类型(static type),或者叫做外观类型(apparent Type);后面的“Man”则称为变量的实际类型(Actual Type)。
静态类型是在编译期可知的,实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么,例如:

    
    Human man = new Man();
    man = new Woman();
    sr.sayHello((Man) man);
    sr.sayHello((Woman) man);
    

使用哪个重载版本,完全取决于入参的数量和数据类型,虚拟机(准确的说是编译器)在重载时是通过参数的静态类型而不是实际类型
作为判断依据的,并且静态类型是编译期可知的,因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。

所依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派典型的应用方法是重载。
静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,

2.2 动态分派
    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 Woman extends Human {
            @Override
            protected void sayHello() {
                System.out.println("woman say hello");
            }
        }
    
        public static void main(String[] args) {
            Human man = new Man();
            Human woman = new Woman();
            man.sayHello();
            woman.sayHello();
            man = new Woman();
            man.sayHello();
        }
    输出结果:  
    man say hello
    woman say hello
    woman say hello
    

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

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

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

2.3 单分派与多分派

静态分派属于多分派类型,因为首先需确实静态类型,然后确定方法参数
动态分派属于单分派类型,因为在执行invokevirtual指令时,已经确定所执行的方法,而可以影响虚拟机选择的因素只有实际类型。

java 1.7 是一个动态单分派、静态多分派的语言

2.4 虚拟机动态分派的实现

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

3、动态类型语言支持

动态类型语言的关键特征是它的类型检查的主题过程是在运行期而不是编译期。

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

介绍虚拟机如何执行方法中的字节码指令
基于栈的指令集(java编译期输出流)
优点:可移植;
缺点:速度慢,因为出栈入栈产生比较多指令,而且栈是在内存中,频繁的栈访问也就是需要频繁的访问内存
寄存器指令集(pc中直接支持的指令集)
由硬件提供,受到硬件约束;

上一篇:spring 源码之模板设计模式


下一篇:Redis-GUI客户端工具Another Redis Desktop Manager介绍