【011】【JVM——虚拟机字节码执行引擎】



JVM——虚拟机字节码执行引擎

Java
虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种版本虚机执行引擎的统一外观(Facade)。在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎。

运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual
Machine Stack )的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code
属性之中,
因此一个栈帧需要分配多少内存,
不会受到程序运行期变量数据的影响,
而仅仅取决于具体的虚拟机实现。

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

栈帧的概念模型如下图所示:

局部变量表

局部变量表(Local Variable Table)
是一组变量值存储空间.用于存放方法参数和方法内部定义的局部变量。在Java
程序编译为Class
文件时,就在方法的Code
属性的max_localso数据项中确定了该方法所需要分配的局部变量表的最大容量。

局部变量表的容量以变盘槽(Variable Slot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot
应占用的内存空间大小。

虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的Slot
数量。64位的数据会使用两个槽。

如果执行的是实例方法(非static
的方法),局部变量表中第0位索引的Slot
默认是用于传递方法所属对象实例的引用,
在方法中可以通过关键字“this”来访问到这个隐含的参散。其余参数则按照参数表顺序排列,占用从l
开始的局部变量Slot,参数表分配完毕后,
再根据方法体内部定义的变量顺序和作用域分配其余的Slot。

类变量有两次赋初始值的过程,
一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。

操作数栈

操作数栈的最大深度在编译的时候写入到code
属性的max_stacks
数据项中。操作数栈的每一个元素可以是任意的Java
数据类型,包括long
和double。32位数据类型所占的栈容量为1。64位数据类型所占的栈容量为2。在方法执行的任意时候,操作数栈的深度都不会超过在max_stacks
数据项中设定的最大值。

概念模型中,两个栈帧作为虚拟机栈的元素是完全相互独立的。但在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。这样在进行方法调用的时候可以共用一部分数据无须进行额外的参数复制传递。

动态链接

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

方法返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(
调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)

另外一种退出方式是,在方法执行过程中遇到了异常,
并且这个异常没有在方法体内得到处理,无论是Java
虚拟机内部产生的异常,还是代码中使用athrow
字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,
就会导致方法退出,
这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,
是不会给它的上层调用者产生任何返问值的。

一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

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

方法调用

方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。一切方法调用在class文件里面存储的都只是符号引用,而不是方法在实际运行时内存中的入口地址。

解析

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

在Java
语言中,符合“编译期可知,运行期不可变”这个要求的方法,主要有静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,
这两种方法都不可能通过继承或别的方式重写出其他版本,因此它们都适合在类加载阶段进行解析。

invokestatic:调用静态方法。

invokespecial:调用实例构造器<init>方法、私有方法和父类方法。

invokevirtual:调用所有的实例方法。

invokeinterface
:调用接口方法,会在运行时再确定一个实现此接口的对象。

Invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4
条调用指令,分派逻辑是固化在Java
虚拟机内部的,而invokedynamic
指令的分派逻辑是向用户所设定的引导方法决定的。在Java
语言规范中明确说明了final方法是一种非虚方法。

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

Java
编译器输出的指令流,是一种基于栈的指令集架构(Instruction SetArchitecture, ISA),指令流中的指令大部分都是零地址指令,它们依赖操作数栈进行工作。

基于栈的指令集主要的优点就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。

使用栈架构的指令集,用户程序不会直接使用这些寄存器,就可以由虚拟机实现来自行决定把一些访问最频繁的数据(程序计数器、栈顶缓存等)放到寄存器中以获取尽量好的性能,这样实现起来也更加简单一些。栈架构的指令集还有一些其他的优点,如代码相对更加紧凑(字节码中每个字节就对应一条指令,而多地址指令集中还需要存放参数)、编译端实现更加简单(不需要考虑空间分配的问题,所需空间都在栈上操作)等。

栈架构指令集的主要缺点是执行速度相对慢一些。栈架构指令集的代码非常紧凑,但是完成相同功能所需要的指令数量一般会比寄存器架构多,因为出栈、入栈操作本身就产生了相当多的指令数量。

时间: 04-08

【011】【JVM——虚拟机字节码执行引擎】的相关文章

深入理解JVM虚拟机5:虚拟机字节码执行引擎

虚拟机字节码执行引擎 微信公众号[Java技术江湖]一位阿里 Java 工程师的技术小站.作者黄小斜,专注 Java 相关技术:SSM.SpringBoot.MySQL.分布式.中间件.集群.Linux.网络.多线程,偶尔讲点Docker.ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!(关注公众号后回复”Java“即可领取 Java基础.进阶.项目和架构师等免费学习资料,更有数据库.分布式.微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的Java学习指南

基于栈的虚拟机字节码执行引擎

一.虚拟机字节码执行引擎概述 虚拟机字节码执行引擎主要就是研究字节码指令具体怎样被执行.对于物理机器,指令的执行是直接建立在OS和硬件的基础上 对于字节码指令的执行就是直接建立在JVM上,然后通过JVM完成具体的字节码指令到机器指令的过程.一般来说虚拟机的执行的 字节码指令是基于栈的不是采用寄存器,主要考虑的原因跨平台. 虚拟机的执行引擎是有JVM规范定义的,可以自己定义指令集以及执行引擎来执行字节码指令.不同的JVM执行引擎的实现可能不同 总体来说一个线程对应的是一个虚拟机栈:线程代码中调用的

虚拟机字节码执行引擎

在前面的几篇文章里,从Java虚拟机内存结构开始,经历了虚拟机垃圾收集机制.Class类文件结构到后来的虚拟机类加载机制,一步步的进入到了Java虚拟机即Java底层的世界.在有了前面的基础之后,接下来就应该进入Java虚拟机最重要的部分了--虚拟机字节码执行引擎,毕竟,这是Java程序得以在不同机器上运行的核心部分. Java是通过实现Java虚拟机来达到平台无关的."虚拟机"的概念是相对于"物理机"来说的,两种机器都有执行代码的能力,不过物理机是直接面向处理器.

Java虚拟机--虚拟机字节码执行引擎

Java虚拟机--虚拟机字节码执行引擎 所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果. 运行时栈帧结构 用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机栈的栈元素.每一个方法从调用开始到执行完成的过程,都对应一个栈帧在虚拟机栈中的入栈出栈过程. 由于虚拟机栈是线程私有的,所以每一个线程都有一个自己的虚拟机栈,而每个虚拟机栈都是由许多栈帧组成.每一个栈帧都包括 局部变量表 操作数栈 动态连接 方法返回地址 额外附加信息 处于

Java虚拟机-字节码执行引擎

概述 Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,成为各种虚拟机执行引擎的统一外观(Facade).不同的虚拟机引擎会包含两种执行模式,解释执行和编译执行. 运行时帧栈结构 栈帧(Stack Frame)支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法的局部变量.操作数栈.动态连接和方法返回地址等信息.方法调用开始到执行完成,对应这一个帧栈在虚拟机栈里面入栈和出栈的过程. 一个线程中

Jvm(59),虚拟机字节码执行引擎----运行时栈帧结构

后面讲的所有的东西就是对前面所总览的虚拟机栈的进一步理解. 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)[1]的栈元素.栈帧存储了方法的局部变量表.操作数栈.动态连接和方法返回地址等信息.每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程. 每一个栈帧都包括了局部变量表.操作数栈.动态连接.方法返回地址和一些额外的附加信息.在编译程序代码的时候,

【JVM】虚拟机字节码执行引擎

概念模型上,典型的帧栈结构如下(栈是线程私有的,也就是每个线程都会有自己的栈). 典型的帧栈结构 局部变量表 存放方法参数和方法内部定义的局部变量.在编译阶段,就在Class文件的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量.(仅仅是变量,不包括具体的对象).</br> 局部变量表内部以变量槽(Variable Slot)为最小单位.对于byte.char.float.int.short.boolean.reference.returnAddress等

Jvm(60),虚拟机字节码执行引擎----局部变量表

在讲这一节之前我们先来抛出一个问题,为什么局部变量必须初始化才能使用,而全局变量却不需要初始化呢? 在这里先写出原因,因为全局变量static一般在类加载器准备的阶段就已经加载到方法区之中了,并且会给它附一个初始化的值比如说0,null之类的让后在把程序员初始化的值付给成员变量.而局部变量却不是这样的,它没有在方法区之中,相对于全局变量,局部变量的生命周期短,声明次数多,如果像全局变量一样给个初始值的话会影响性能,不给初始值又不安全,所以折中了一下,规定了用户需要先赋值再使用.如果没有初始化,类

008 虚拟机字节码执行引擎

执行引擎是Java虚拟机最核心的组成部分之一. 物理机的执行引擎建立在处理器.硬件.指令集和操作系统之上的,虚拟机的执行引擎需要自己实现,因此可以自己制定指令集与执行引擎的结构体系,并且支持那些不被硬件直接支持的指令集格式. 1.运行时栈帧结构 栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区域的虚拟机栈的栈元素. 栈帧存储了方法的局部变量表.操作数栈.动态连接和方法返回地址. 对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,成为当前栈帧(Current