Understanding the JVM(十二)虚拟机字节码执行引擎

java虚拟机的执行引擎:输入字节码文件(.class文件),通过解释执行或者编译执行进行字节码解析,输出执行结果。

一 . 运行时栈帧结构

=======

  • 栈帧是虚拟机栈的栈元素,栈帧存储了局部变量表,操作数栈,动态连接,和返回地址等。
  • 每一个方法的执行 对应的一个栈帧在虚拟机里面从如栈到出栈的过程。
  • 栈帧分配的内存在编译代码的时候已经确定了,不会受程序运行期变量的影响,仅仅取决于具体的虚拟机实现。
  • 只有位于栈顶的栈帧才有有效的,对应的方法称为当前方法。 执行引擎运行的所有指令只针对当前栈帧和当前方法。

1)局部变量表

  • 局部变量表存放的一组变量的存储空间。存放方法参数和方法内部定义的局部变量表。
  • 在java编译成class的时候,已经确定了局部变量表所需分配的最大容量。
  • 局部变量表的最小单位是一个Slot。虚拟机规范没有明确规定一个Slot占多少大小。只是规定,它可以放下boolean,byte,…reference,return address。允许slot长度随着处理器操作系统或者虚拟机的不同发生变化。即使在64位的虚拟机中使用64位的物理空间去实现一个slot,虚拟机使用对齐或者补白的手段让slot在外观上和32位的一致。一个slot可以放下32位的数据类型,对于64位的数据类型(明确指出的是long和double类型),会以高位对齐的方式为其分配两个连续的slot空间。
  • 局部变量表的读取方式是索引,从0开始。对于实例方法,局部变量表中的第0位索引的slot默认是所属实例对象的引用,在方法中可以通过关键字this来访问。
    局部变量表的分配顺序如下:
    根据局部变量顺序,分配Solt。一个变量一个solt,64为的占2个solt。java中明确64位的是long & double
  • 为了尽可能的节约局部变量表,Solt可以重用。如果当前字节码PC计数器的值已经超过某个作用域,那这个变量对应的slot就可以交给其他变量使用。

注意:局部变量只给予分配的内存,没有class对象的准备阶段,所以局部变量在使用前,必须先赋值。

2)操作数栈

操作数栈在概念上很像寄存器。java虚拟机无法使用寄存器,所以就有操作数栈来存放数据。
虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。
比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,
它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:

begin

iload_0 // push the int in local variable 0 onto the stack

iload_1 // push the int in local variable 1 onto the stack

iadd // pop two ints, add them, push result

istore_2 // pop int, store into local variable 2

end

操作数栈 的数据读取、写入就是出栈和如栈操作。

3)动态连接

每个栈帧都包含一个指向运行时常量池的引用,持有这个引用是为了支持动态连接。
符号池的引用,有一部分是在第一次使用或者初始化的时候就确定下来,这个称为静态引用。
还有一部分是在每次执行的时候采取确定,这个就是动态连接。
异常情况:方法不会返回任何值,返回地址有异常表来确定,栈帧一般不存储信息。

4)方法返回地址

方法只有2中退出方式,正常情况下,遇到return指令退出。还有就是异常退出。
正常情况:一般情况下,栈帧会保存调用者PC计数器的值。虚拟机通过这个方式,执行方法调用者的地址,然后把返回值压入调用者中的操作数栈。

5)附加信息

与调试相关的等规范里面没有描述的信息。

二.方法调用

方法调用阶段不是执行该方法,而仅仅时确认要调用那个方法。class文件在编译阶段没有连接这一过程,所以动态连接这个在C++就已经有的技术,在java运用到了一个新的高度。所有的函数(除了私有方法,构造方法 & 静态方法,下同),理论上都可以时C++里面的虚函数,这些函数都需要通过动态绑定来确定“明确”的函数实体。

1)解析

所有方法在class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是,方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。
Java虚拟机提供5中方法调用命令:
invokestatic:调用静态方法
invokespecial:调用构造器,私有方法和父类方法
invokevirtual:调用虚方法
invokeinterface:调用接口方法
invokedynamic:现在运行时动态解析出该方法,然后执行。
被invokestatic invokespecial指令调用的方法,都是在解析阶段确定唯一的调用版本,将符号解析为该方法的直接引用。符合的有静态方法,私有方法,实例构造器和父类方法。
java规定 final修饰的是一种非虚方法。

2)分派

1.静态分派
依赖静态类型来定位方法执行版本的分派动作,称为静态分派。静态分派的最典型的应用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。编译器虽然能确定方法的重载版本,但很多情况下这个重载版本并不是唯一的,往往只能确定一个更加合适的版本,原因是字面量不需要定义,所以字面量没有显式的静态类型。

2.动态分派

package com.joyfulmath.jvmexample.dispatch;

import com.joyfulmath.jvmexample.TraceLog;

/**
 * @author deman.lu
 * @version on 2016-05-19 14:26
 */
public class DynamicDispatch {
    static abstract class Human{
        protected abstract void sayHello();
    }

    static class Man extends Human{

        @Override
        protected void sayHello() {
            TraceLog.i("Hello gentleman!");
        }
    }

    static class Woman extends Human{

        @Override
        protected void sayHello() {
            TraceLog.i("Hello lady!");
        }
    }

    public static void action()
    {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }
}

invokevirtual动态查找指令:
invokevirtual指令解析的过程大概如下:首先在操作数栈里第一个元素的实际类型,即为C。
如果在类型C中找到与常量描述符相同的类名和方法,则权限校验通过后,即为找到该法方法,则返回这个方法的直接引用。
否则,对C的父类进行依次查找。
这个过程通俗一点就是,先从当前类里面寻找“同名”的该方法,如果没有,就从C的父类里面找,知道找到为止!
这个找到的方法,就是我们实际要调的方法。
如果找不到,就是exception。一般情况下,编译工具会帮我们避免这种情况。
3.单分派与多分派
重载和重写都存在的情况。

package com.joyfulmath.jvmexample.dispatch;

import com.joyfulmath.jvmexample.TraceLog;

/**
 * @author deman.lu
 * @version on 2016-05-19 15:02
 */
public class MultiDispatch {
    static class QQ{}
    static class _360{}

    public static class Father{
        public void hardChoice(QQ qq){
            TraceLog.i("Father QQ");
        }

        public void hardChoice(_360 aa){
            TraceLog.i("Father 360");
        }
    }

    public static class Son extends Father{
        public void hardChoice(QQ qq){
            TraceLog.i("Son QQ");
        }

        public void hardChoice(_360 aa){
            TraceLog.i("Son 360");
        }
    }

    public static void action()
    {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new _360());
        son.hardChoice(new QQ());
    }
}

编译阶段编译器的选择过程,也就是静态分派过程,1.静态类型 2.方法参数。两条invokevirtual,参数分别为Father.hardChoice(360) Father.hardChoice(QQ) 两种符号引用。根据两个宗量进行选择,静态分派属于多分派。
运行阶段虚拟机的选择,动态分派,方法接收者的实际类型,属于单分派
4.虚拟机动态分派的实现
由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正的进行如此频繁的搜索。面对这种情况,最常用的”稳定优化“手段就是为类在方法区中建立一个虚方法表(Virtual Method Table,也称为vtable),使用虚方法表索引来代替元数据查找以提高性能。
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都是指向父类的实际入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实际版本的入口地址。

三.基于栈的字节码解释执行引擎

如今,基于物理机、Java虚拟机,或者非Java的其他高级语言虚拟机(HLLVM )的语 言 ,大多都会遵循这种基于现代经典编译原理的思路,在执行前先对程序源码进行词法分析和语法分析处理,把源码转化为抽象语法树( Abstract Syntax Tree,AST)。
1. C/C++语言,词法分析、语法分析以至后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整意义的编译器去实现,这类代表是C/C++语言。
2. java语言把其中一部分步骤(如生成抽象语法树之前的步骤)实现为一个半独立的编译器,这类代表是Java 语言。
3. javascript把这些步骤和执行引擎全部集中封装在一个封闭的黑匣子之中,如大多数的JavaScript执行器。
这里写图片描述
Java语言中 ,Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。因为这一部分动作是在Java虚拟机之外进行的, 而解释器在虚拟机的内部,所以Java程序的编译就是半独立的实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值