JVM学习--(五)虚拟机字节码执行引擎

JVM学习–(五)虚拟机字节码执行引擎

执行引擎是Java虚拟机核心的组成部分之一,所有虚拟机的概念模型都是相同的,但是在实现的过程中,不同的虚拟机不同,通常会有解释执行以及编译执行,也可以两者兼得。从外观上来看,所有虚拟机的执行引擎都是相同的,因为他们都是输入字节码的二进制流,输出的是执行结果。

一.运行时栈帧结构

“栈帧”是用来支持虚拟机进行方法调用和方法执行背后的数据结构,其中储存了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。对于执行引擎而言,只有位于栈顶的方法才是在运行的,被称为“当前栈帧”,与此关联的是“当前方法”。

1.局部变量表

是一组变量值的储存空间,用来存放方法参数和方法内部定义的局部变量。变量槽是最小的单位,变量槽的大小跟物理机有关。一个变量槽可以储存不超过32位存储空间的数据,包括boolean,byte,char,short,int,float,reference和returnAddress,其中reference表示对于一个实例对象的引用,至少可以做到两件事:一是可以根据引用间接或者直接查找到对象在java堆中的数据存放的起始地址和索引,二是可以根据引用间接或者直接查找到对象所属数据类型在方法去中的储存的类型信息。对于64位的数据类型只有long和double,允许把一次long和double数据类型读写分割成两次32位读写的做法。
局部变量表中的第0位索引是对于传递方法所属对象实例的引用,在方法中通过this可以访问到这个对象。
变量槽是可以复用的,而且会影响到gc的进行。这时候可以通过将不使用得对象手动设置为null来解除引用使得其可以被回收,但是这种方法可能被编译优化为无效命令而删除。
**局部变量若没有被赋初始值是完全不能使用的。

2.操作数栈

是一个后入先出的栈,同样32位数据占1个栈,而64位数据需要占2个栈。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配。
不同栈帧作为不同方法的虚拟机栈的元素是完全独立的,但是大多数虚拟机都会进行优化,将上个栈帧的局部变量表共享区域与下个栈帧的操作栈共享区域重叠。

3.动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。一部分的符号引用会在类加载阶段就转成直接引用,这被称为静态解析;而另外一部分将在每一次运行期间都转成直接引用,这部分被称为动态连接。

4.方法返回地址

一个方法开始执行,只有两种方法退出。一种是遇到任意一个方法返回的字节码指令。另一种是遇到了异常。
无论怎么退出,在方法退出之后都需要返回到最初方法被调用的位置。因此需要栈帧中保留一点信息来帮助恢复它的上层主调方法的执行状态。
因此一般来说退出会引发的操作:1.恢复上层方法的局部变量表和操作数栈;2.把返回值踏入调用者栈帧操作数栈中;3.调整PC计数器的值以指向方法调用指令后面的一条指令。

二.方法调用

唯一的任务是确定被调用方法的版本。

1.解析

在类加载的解析阶段,会将其中的一部分符号引用转化成直接引用,这种解析的成立前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。Java中符合“编译器可知,运行期不可变”这个要求主要有静态方法和私有方法两大类,这两种都适合在类加载阶段进行解析。调用不同类型的方法,字节码设计了不同的指令,java虚拟机支持以下5种方法调用字节码指令,分别是:
1.invokestatic,用于静态方法。
2.invokespecial,用于实例构造器,私有方法,父类中的方法。
3.invokevirtual,用于调用所有的虚方法。
4.invokeinterface,用于调用接口方法,会在运行时再确定一个实现该接口的对象。
5.invokedynamic,动态调用。
可以被1,2两种调用的以及final修饰的都可以在解析阶段就确定唯一的调用版本,被称为“非虚方法”。

2.分派

分派是“重载”和“重写”在Java虚拟机中的实现。
①静态分派
例如Human man=new Man();中Human是“静态类型”,而Man是“实际类型”,静态类型是编译期可知的,但实际类型确是运行时才可以确定的。虚拟机在进行重载的时候是通过参数的静态类型来作为判断依据的。
所有依赖静态类型来决定方法执行版本的分派动作被称为静态分派,最典型的就是重载;依赖动态类型来决定方法执行的是动态分派。
很多情况下重载版本并不是唯一的,只是一个更“适合”的版本。其中涉及的就是自动类型转换,例如char>int>long>float>double>Character>Serializable。
②动态分派
动态分派与“重写”有着密切的关系。
invokevirtual指令在运行解析过程中分为以下几步:
1.找到操作数栈顶的第一个元素所指向的对象的实际类型,记为C。
2.如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验。
3.否则,按照继承关系从下到上依次对C的各个父类进行第二轮的搜索。
4.如果没有合适的方法,则抛出异常。
**字段不参加多态。哪个类的方法访问某个名字的字段时,该名字指的就是这个类能看到的那个字段。
③单分派和多分派
方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派分成单分派和多分派。
静态分派属于多分派类型。
动态分派属于单分派类型,只与方法的接收者有关。
④虚拟机动态分派的实现
常见的优化手段是为类型在方法区中建立一个虚方法表,用虚方法表来代替元数据查找来提高性能。
虚方法表中各个方法的实际入口地址,如果某个方法在子类中没有被重写,那么子类的虚方法的地址入口跟父类相同方法的地址入口是一致的。

三.动态类型语言支持

jdk7发布的invokedynamic。
动态语言的关键特征是它的类型检查的主体过程是在运行期而不是在编译器进行的。
java语言在编译期间已经方法的符号引用生成出来,并且作为方法调用指令储存在Class文件中。
动态类型语言的一个核心特征是“变量无类型而变量值才有类型”。

invoke包

提供了一种新的动态确定目标方法的机制,称为“方法句柄”。
methodhandle和reflection的区别:
1.Reflection是在模仿java代码层次的方法调用,而methodHandle是模仿字节码层次的方法调用。
2.Reflection中的Method方法包含的信息比MethodHandle的Method方法包含的信息多,前者是Java端的全面映射,而后者仅仅是包含执行该方法的相关信息。
3.MethodHandle是对字节码的调用模拟,因此可以用到很多字节码层的优化方法,但是反射就不能。

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

不同的虚拟机的实际模型都是有很大区别的,但是他们的概念模型都是一样的。

1.解释执行

大部分JAVA虚拟机都遵循:程序源码–词法分析–单词流–语法分析–抽象语法树–指令流–解释器–解释执行。
这些步骤可以独立于执行引擎,这就是C++,而可以将其中的一部分实现半独立的编译器,就是Java语言。Java语言中到指令流是在javac编译器中完成的,而解释器在虚拟机的内部。

2.基于栈的指令集与基于寄存器的指令集

Javac编译器输出的字节码指令流是一种基于栈的指令集架构,大部分都是零地址指令,依赖操作数栈进行工作。另一种就是基于寄存器的指令集,其中比较典型的是x86的二地址指令集:
mov eax,1
add eax,1
两者差别:栈指令的主要优点是可以移植,缺点是速度稍微慢一点,主要因为完成相同功能的指令数量比寄存器架构来的多,以及栈是在内存中的,频繁访问降低速度。寄存器指令反之。

3.基于栈的执行引擎

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值