Jvm笔记总结(十):虚拟机字节码执行引擎

Jvm笔记总结(十):虚拟机字节码执行引擎

PS : 本文乃学习整理参考而来 ,目录参考 [ Jvm系列目录 ]


执行引擎是Java虚拟机最核心的组成部分之一。虚拟机是一个相对于物理机的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎则是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能执行那些不被硬件直接支持的指令集格式。在不同虚拟机实现里面执行引擎执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过及时编译器产生本地代码执行)两种选择,也可能两者兼备HotSpot两者兼备,JRockit只存在及时编译器,只能编译执行


栈帧(Stack Frame):存储了方法的局部变量表操作数栈动态连接方法返回地址等信息。

         栈帧是用于支持虚拟机进行方法调用方法执行数据结构,他是虚拟机栈栈元素栈帧存储了方法局部变量表操作数栈动态连接方法返回地址等信息。每个方法从调用到完成,都对应这一个栈帧在虚拟机栈里面从入栈到出栈的过程。在编译程序代码(编译成Class文件时)的时候栈帧中需要多大局部变量表多深的操作数栈都已经完全确定了,并且写入方法表(Class结构组成部分)的Code属性之中,因此一个栈帧需要分配多少内存不会受到程序运行期变量数据的影响,而仅仅取决于虚拟机实现

         一个线程中的方法调用链可能会很长很多方法都同时处于执行状态对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引擎运行的所有字节指令只针对当前栈帧进行操作。

局部变量表(Local Variable Table):是一组变量值存储空间,用于存放方法参数方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法表的Code属性max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量局部变量表的容量以变量槽(Variable Slot)为最小单位(Slot),虚拟机规范中并没有明确指明一个Slot应占用内存空间大小,只是很有导向性的说到每个Slot都应该能存放一个booleanbyteshortintfloatreference或returnAddress类型的数据,这8种数据类型,都可以使用32位(4个字节)或更小的物理内存来存放。其中reference类型表示对一个对象实例的引用,虚拟机规范没有说明其长度,也没有指出该引用的具体结构。但一般来说,虚拟机实现至少应当做到两点一是此引用能直接间接查找到对象在Java堆中的数据存放的起始地址索引二是此引用能直接或间接的查找到对象所属数据类型在方法区的存储的信息。而returnAddress已基本弃用,原本用来实现异常处理,现在已经有异常表代替。Java中明确的64位的数据类型只有longdouble两种。
         在方法执行时,如果执行的是实例方法,那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”(这也是实例方法的方法表Code属性中参数大于1的原因来访问到这个隐含的参数其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配

操作数栈(Operand Stack):也常称为操作栈,他是一个先进后出栈同局部变量表一样,操作数栈的最大深度也在编译时写入到Code属性的max_stacks数据项中在方法执行的任何时候,操作数栈的深度都不会超过max_stacks数据项中设定的最大值。当一个方法刚刚开始执行时,操作数栈是空的,在方法执行过程中,会有各种字节码指令向操作数栈写入和提取内容,也就是出栈/入栈操作Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“”就是操作数栈
例如:在执行整数加法指令iadd时,操作数栈中最近接栈顶的两个元素已经存入了两个int型的数值,当iadd执行时,会将这两个int值出栈相加,然后将结果入栈

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

方法返回地址:当一个方法开始执行后,只有两种方式可以退出这个方法。第一种执行引擎遇到一个方法返回的字节码指令这种退出方式称为正常完成出口(Normal Method Inocation Completion)。另一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论何种异常,只要在本方法异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。方法退出实际上等同于当前栈帧出栈,因此退出时可能执行的操作有恢复上层方法的局部变量表和操作数栈把返回值压入调用这栈帧操作数栈中,调整PC计数器的值指向方法调用指令后面的一条指令

附加信息:一般把动态连接方法返回地址与其他附加信息全部归为一类,称为栈帧信息


方法调用

         方法调用不等同方法执行方法调用阶段唯一的任务就是确定被调用方法的版本即调用哪个方法),不涉及方法内部的具体运行过程。因为Java文件编译成Class文件的过程不包含连接步骤,一切方法调用Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局的入口地址直接引用)。这一特性给Java带来了强大的动态扩展能力,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用方法调用 分为:解析调用(静态)、分派调用(静态或动态)。

解析:叫解析调用更准确。所有方法调用中的目标方法Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将一部分符号引用转化为直接引用这种解析能成立的前提是方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可以变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析。在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法私有方法两大类以及final修饰的方法),因为staticfinal以及private修饰的方法不能被重写(final和static修饰的方法不能重写, 可以重载),因此被唯一确定所以都适合类加载阶段进行解析。
在Java虚拟机中提供了5种方法调用字节码指令(助记符):

1.invokestatic:调用静态方法。
2.invokespecial:调用实例构造器<init>方法私有方法父类方法
3.invokevirtual:调用所有虚方法
4.invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
5.invokedynamic:Java语言中不用虚拟机为了支持动态语言而存在。

只要能被invokestaticinvokespecial指令调用的方法,都可以在解析阶段确定唯一调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类,他们在类加载的时候就会把符号引用解析为该方法的直接引用。这类方法称为非虚方法,与之相反称为虚方法(除final方法,虽然使用invokevirtual来调用)。
         解析调用一定是个静态的过程,在编译期就完全确定,在类加载解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成

分派分派调用。Java是一门面向对象的程序语言,具备面向对象的3个基本特征:继承、多态、封装。而分派将会揭示多态性特征的基本体现,如“重载”和“重写”Java虚拟机之中是如何实现的。
分派又分为静态分派动态分派

1.静态分派
2.动态分派

静态分派
如下重载代码:

public class Test {


    static abstract class Human {

    }

    static class Man extends Human {

    }

    static class Woman extends Human {

    }

    public void sayHello(Human human){
        System.out.println("hello,human!");
    }
    public void sayHello(Man man){
        System.out.println("hello,man!");
    }
    public void sayHello(Woman woman){
        System.out.println("hello,woman!");
    }

    public static void main(String[] args) {

       Human man = new Man();
       Human woman = new Woman();
       Test ts = new Test();
       ts.sayHello(man);
       ts.sayHello(woman);
    }
}

输出结果:
hello,human!
hello,human!

如上代码中:Human man = new Man();
把“Human”称为变量的静态类型(Static Type),或者叫外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Tpye)。静态类型在编译器可知的,而实际类型在运行期才可确定编译器在编译程序的时候并不知道一个对象实际类型是什么。而在虚拟机(准确的说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判断依据返回值不参与重载选择)。并且静态类型是编译器可知的,因此在编译阶段Javac会根据参数的静态类型决定使用哪个重载版本
所有依赖静态类型来定位方法执行版本的分派动作成为静态分派静态分派的典型应用方法重载静态分派发生在编译阶段,因此确定静态分派的动作实际不是由虚拟机来执行而是编译器

动态分派:他是多态性的另外一个重要体现——重写(Override)有着密切的关系。
如下重载代码:

public class Test {

    static abstract class Human {
        protected abstract void sayHello();
    }

    static class Man extends Human {

        @Override
        protected void sayHello() {
            System.out.println("man sayHello!");
        }
    }

    static class Woman extends Human {
        @Override
        protected void sayHello() {
            System.out.println("woman sayHello!");
        }
    }


    public static void main(String[] args) {

       Human man = new Man();
       Human woman = new Woman();
       man.sayHello();
       woman.sayHello();
    }
}

输出结果:
man sayHello!
woman sayHello!

        动态分派不会再根据静态类型来决定而取决于这两个变量实际类型不同。原因要从虚方法invokevirtual指令的多态查找过程,大致为以下步骤:

1.找到操作数栈顶的第一个元素所指向对象实际类型,记作C。
2.如果在类型C中找到相符的方法,则进行访问权限校验
3.否则,按照继承关系从下往上一次对C的父类进行搜索。
4.否则,抛出异常:AbstractMethodError。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值