关于字节码执行引擎

在不同的虚拟机实现中, 执行引擎在执行字节码的时候, 通常会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行) 两种选择,也可能两者兼备

栈中首先都是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

栈帧

栈帧是用于支持虚拟机进行方法调用和方法执行背后的数据结构,一个方法对应一个栈帧,每一个方法从调用开始至执行结束的过程, 都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。它也是虚拟机运行时数据区中的虚拟机栈的栈元素;

每一个栈帧都包括了局部变量表、 操作数栈、 动态连接、 方法返回地址和一些额外的附加信息,其大小在编译期间确定;

对于执行引擎来讲, 在活动线程中, 只有位于栈顶的方法才是在运行的, 只有位于栈顶的栈帧才是生效的, 其被称为“当前栈帧”,与这个栈帧所关联的方法被称为“当前方法” ;

 

局部变量表

局部变量表是一组变量值的存储空间, 用于存放方法参数和方法内部定义的局部变量,变量槽的长度可以随着处理器、 操作系统或虚拟机实现的不同而发生变化

一个32位的变量槽可以放boolean、 byte、 char、 short、 int、float、 reference和returnAddress这8种类型,long和double占用两个变量槽;

局部变量表采用索引分配,变量分配顺序?

局部变量表中的变量槽是可以重用的,当出了变量的作用范围后,其变量槽位置可以被其他变量占用。

 

操作数栈

操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。 32位数据类型所占的栈容量为1, 64位数据类型所占的栈容量为2。

操作数栈采用后入先出分配,一开始操作数栈是空的,在方法的执行过程中, 会有各种字节码指令往操作数栈中写入和提取内容, 也就是出栈和入栈操作。

操作数栈中的数据和局部变量表中的数据通过字节码指令来回复制。操作数栈的功能是对变量进行操作,局部变量表的功能是存储变量。

通常两个栈帧之间的操作数栈和局部变量表会有重叠区域,好处是?

 

动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用, 持有这个引用是为了支持方法调用过程中的动态连接

这些符号引用一部分会在类加载的解析阶段或者第一次使用的时候就被转化为直接引用, 这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用, 这部分就称为动态连接

 

方法返回地址

方法有正常退出和异常退出两种,无论何种退出方式, 在方法退出之后, 都必须返回到最初方法被调用时的位置。正常退出前,主调方法的pc计数器信息会被栈帧保留以便作为返回地址;异常退出的返回地址通过异常处理器表确定。

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

 

方法调用阶段

方法调用阶段不等于执行方法体,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法) , 暂时还未涉及方法内部的具体运行过程

 

解析

在类加载的解析阶段, 会将其中的一部分符号引用转化为直接引用, 解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。 换句话说, 调用目标在程序代码写好、 编译器进行编译那一刻就已经确定下来,解析调用一定是个静态的过程, 在编译期间就完全确定。

 

非虚方法:

符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法,这5种方法调用会在类加载解析的时候就可以把符号引用解析为该方法的直接引用。

 

分派

静态分派跟方法重载有密切联系

虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。由于静态类型在编译期可知, 所以在编译阶段, Javac编译器就根据参数的静态类型决定了方法调用时会使用哪个重载版本

 

动态分派跟方法重写有密切联系

虚方法

动态分派关键在于invokevirtual指令,invokevirtual在运行期间的解析过程为:

1) 找到操作数栈顶的第一个元素所指向的对象的实际类型, 记作C。

2) 如果在类型C中找到与常量中的描述符和简单名称都相符的方法, 则进行访问权限校验, 如果

通过则返回这个方法的直接引用, 查找过程结束; 不通过则返回java.lang.IllegalAccessError异常。

3) 否则, 按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。

4) 如果始终没有找到合适的方法, 则抛出java.lang.AbstractMethodError异常。

 

正是因为invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型, 所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了, 还会根据方法接收者的实际类型来选择方法版本, 这个过程就是Java语言中方法重写的本质。 我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派

 

字段没有动态分派,只跟调用者的编译类型有关。

 

方法的宗量

方法的接收者与方法的参数统称为方法的宗量

单分派是根据一个宗量对目标方法进行选择, 多分派则是根据多于一个宗量对目标方法进行选择。

Java语言的静态分派属于多分派类型,Java语言的动态分派属于单分派类型

 

虚拟机的动态分派

一种基础而且常见的优化手段是为类型在方法区中建立一个虚方法表

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

虚方法表一般在类加载的准备阶段进行初始化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值