JVM总结(五):JVM字节码执行引擎

JVM字节码执行引擎
  运行时栈帧结构
    局部变量表
    操作数栈
    动态连接
    方法返回地址
    附加信息
  方法调用
    解析
    分派 –“重载”和“重写”的实现
      静态分派
      动态分派
      单分派和多分派
      JVM动态分派的实现
  基于栈的字节码解释执行引擎
      基于栈的指令集与基于寄存器的指令集

JVM字节码执行引擎

  虚拟机是相对于“物理机”而言的,这两种机器都有代码执行能力,其区别主要是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎是自己实现的。因此程序员可以自行制定指令集和执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。 
  在Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型称为各种虚拟机执行引擎的统一外观。虚拟机实现中,可能会有两种的执行方式:解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码)。有些虚拟机值采用一种执行方式,但是有点采用了两种,甚至有可能包含几个不同级别的编译器执行引擎。 
  所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件、处理过程是等效字节码解析过程,输出的是执行结果。

运行时栈帧结构

  栈帧(Stack Frame)是一种数据结构,它主要是用来支持虚拟机进行方法调用和方法执行。它是虚拟机运行时数据区的虚拟机栈的栈元素。 
  包含内容:栈帧包含了局部变量表、操作数栈、动态连接、方法返回地址一些额外的附加信息等。 
  执行过程:一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。在活动线程中,只有栈顶的栈帧才是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法,执行引擎所运行的所有的字节码指令都只针对当前栈帧进行操作。 
  执行意义:每个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

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

局部变量表

  一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中确定了改方法所需分配的最大局部变量表的容器。 
  包含类型:boolean、byte、char、short、int、float、reference或returnAddress类型八种类型。 
  容量单位:变量槽(slot)。不过虚拟机中并没有明确确定每一个变量槽所占据的内存空间大小,只是有导向性地说明每个变量槽都应该存放的八种类型:boolean、byte、char、short、int、float、reference或returnAddress类型的数据。这种描述和明确指出“每个Slot占用32位长度的内存空间”有一些差别,它允许Slot的长度随着不同的处理器、操作系统或者虚拟机而发生改变。在64位系统上使用64位长度的内存空间来实现一个slot,虚拟机仍要使用对齐和补白的手段让Slot在外观上看起来与32位虚拟机中的一致。

  在Java中占32位以内的数据类型有boolean、byte、char、short、int、float、reference或returnAddress类型等,前六种不解释,而后面的reference是对象的引用。虚拟机规范并没有说明它的长度,也没有明确指出这个引用应有怎样的结构,但一般来说:虚拟机实现至少都应当能从此引用中直接或间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。而returnAddress是为字节码指令jsr、jsr_w和ret服务的,它指向一条字节码指令的地址。 
对于64为的数据类型,虚拟机会以高位在前的方式为其分配两个连续的Slot空间。即long和double两种类型。做法是将long和double类型速写分割为32位读写的做法。不过由于局部变量表建立在线程的堆栈上,是线程的私有数据,无论读写两个连续的Slot是否是原子操作,都不会引起数据安全问题。

  虚拟机索引方式:虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的Slot数量。如果是32为数据类型的数据,索引n就表示使用第n个Slot,如果是64位数据类型的变量,则说明要使用第n和第n+1两个Slot。 
在方法执行过程中,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程。如果是实例方法(非static方法),那么局部变量表中的第0位索引的Slot默认是用来传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含的参数。其余参数按照参数表的顺序来排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。 
局部变量表中的Slot是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超过了某个变量的作用域,那么这个变量相应的Slot就可以交给其他变量去使用。节省栈空间。但也有可能会影响到系统的垃圾收集行为。

  还有一点要说明的是:局部变量不像前面介绍的类变量那样存在“准备阶段”。我们知道,类变量在加载过程中要经过两次赋初始值的过程:一次在准备阶段,赋予系统初始值,另外一次在初始化阶段,赋予程序员定义的初始值。但局部变量不一样,如果一个局部变量定义了但是没有赋初始值是不能使用的。所有不要认为Java中任何情况下都存在着诸如整型变量默认为0,布尔型变量默认为false之类的默认值。这一点要好好注意一下。

操作数栈

  操作栈,它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。 
操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位所占的栈容量为2.在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。

  当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈操作。 
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。 
  另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的。但是大多数的虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。这样在进行方法调用时就可以共用一部分数据,而无须进行额外的参数复制传递。

  Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中的栈就是指操作数栈。

动态连接

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

方法返回地址

  当一个方法被执行后,有两种方式可以退出这个方法。 
  第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。 
  另外一种退出方式是:在方法执行过程中遇到异常,并且这个异常没有在方法体内得到处理,无论是JVM内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。这种方式被称为异常退出出口。此方式不会给上层调用者产生任何返回值。

  无论采用哪一种退出方式,在方法退出后,都会返回到方法被调用的位置,程序才能继续执行。方法返回时可能要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出之后,调用者的PC计数器的值就可以作为返回地址。栈帧中很可能会保存这个计数器值,而方法异常退出后,返回地址就要通过异常处理器表来确定,栈帧一般不保存这部分信息。 
  方法退出实际上就是把当前栈帧出栈的操作:因此退出时可能执行的操作:恢复上层方法局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向指令后面的一条指令。

附加信息

  增加一些没有描述的信息到栈帧之中。一般将动态连接、方法返回地址和其他附加信息全部归为一类,称为栈帧信息。

方法调用

  Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用都在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前所说的直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。

解析

  所有的方法调用的目标方法在Class文件里面都只是一个常量池的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是L方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不会改变的。换句话说:调用目标在程序代码写好、编译器进行编译时就必须确定下来,这类方法的调用称为解析。 
在JVM中提供了5条方法调用字节码指令,分别是: 
invokestatic:调用静态方法 
involespecial:调用实例构造器方法、私有方法和父类方法。 
invokevirtual:调用所有的虚方法。 
invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。 
invokedynamic:先在运行时动态解析出调用限定符所引用的方法,然后再执行该方法。 
  只要能被invokestatic和invokeapecial指令调用的方法,都是可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类,他们在类加载的时候就会把符号引用解析为该方法的直接引用,这些方法可以称为非虚方法,与之相反,其他方法称为虚方法(除去final方法)。非虚方法除了上述的两种以外,还有一种就是被final修饰的方法,虽然final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,所以可以把final方法看作是一种非虚方法。 
  解析调用一定是个静态的过程,在编译期间就可以确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。而分派调用则可能是静态的或者是动态的,根据分派依据的总量数可以分为单分派和多分派,这两种分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派和动态多分派四种分派组合。

分派 –“重载”和“重写”的实现

静态分派

  JVM在重载时是通过参数的静态类型而不是实际类型做判定的,并且静态类型是编译期可知的,因此在编译阶段,Javac编译器会根据参数的静态类型决定使用那个重载版本,然后再把该方法的符号引号写到main()方法的两条invokevritual指令的参数中。 
  所有依赖于静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型是方法的重载。 
  静态分派发生的时间:静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。 
  静态方法会在类加载期进行解析,而静态方法显然是可以拥有重载版本的,选择重载版本的过程也是通过静态分派完成的。

动态分派

  运行期根据实际类型确定方法执行版本的分派过程称为动态分派。动态分派与方法重写有着密切的关系。

单分派和多分派

  方法的接收者与方法的参数统称为方法的宗量。而根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种,单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。

JVM动态分派的实现

  由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,为了避免频繁的搜索,最常用的“稳定优化”的手段就是为类在方法区中建立一个虚方法表,使用虚方法表索引来代替元数据查找以提高性能。 
  虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都是指向父类的实现入口,如果子类中重写了这个方法,子类方法表中的地址将会替换成指向子类实现版本的入口地址。 
  为了程序实现上的方便,具有相同的签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更在、查找的方法表,就可以从不同的虚方法表中按照索引转换出所需的入口地址。

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


  Java程序在执行前先对程序源码进行词法分析和语法分析处理,把源码转化为抽象语法树。对于一门具体语言的实现来说,词法分析、语法分析以及后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整意义的编译器去实现,这类代表是C/C++语言。当然也可以选择其中的一部分步骤实现一个半独立的编译器,这类代表是Java语言。又或者把这些步骤和执行引擎全部集中封装到一个封闭黑匣子中,如大多数的JS执行器。

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

  Java编译器输出的指令流,基本上是一种基于栈指令集架构,指令流中的指令大部分都是零地址指令,它们依赖操作数栈进行工作。 
  基于栈的指令集主要优点就是可移植。除此之外,还有其他的优点,如代码相对更加紧凑(字节码中每个字节就对应一条指令,而多地址指令集中还需要存放参数)、编译器实现更加简单等。 
  缺点是:执行速度相对较慢。

时间: 2024-11-10 20:46:22

JVM总结(五):JVM字节码执行引擎的相关文章

JVM虚拟机(二):字节码执行引擎

运行时栈帧结构 ? ? 栈帧时用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈的栈元素.栈帧存储了方法的局部变量表.操作数栈.动态链接.和方法返回地址等信息. 局部变量表 ??局部变量表的容量以变量槽为最小单位.每个变量槽应该能存放一个boolean.byte.char.short.int.float.reference或returnAddress(可忽略,现在已经很少见了).reference类型表示对一个对象实例的引用,即1 根据引用直接或间接的查到对

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

 JVM--虚拟机字节码执行引擎 Java 虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种版本虚机执行引擎的统一外观(Facade).在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎. 运行时栈帧结构 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈

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

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

JAVA 虚拟机类加载机制和字节码执行引擎

引言 我们知道java代码编译后生成的是字节码,那虚拟机是如何加载这些class字节码文件的呢?加载之后又是如何进行方法调用的呢? 一 类文件结构 无关性基石 java有一个口号叫做一次编写,到处运行.实现这个口号的就是可以运行在不同平台上的虚拟机和与平台无关的字节码.这里要注意的是,虚拟机也是中立的,只要是符合规范的字节码,都可以被虚拟机接受,例如Groovy,JRuby等语言,都会生成符合规范的字节码,然后被虚拟机所运行,虚拟机不关心字节码由哪种语言生成. 类文件结构 class类文件是一组

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

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

深入理解Java虚拟机(类文件结构+类加载机制+字节码执行引擎)

周志明的<深入理解Java虚拟机>很好很强大,阅读起来颇有点费劲,尤其是当你跟随作者的思路一直探究下去,开始会让你弄不清方向,难免有些你说的啥子的感觉.但知识不得不学,于是天天看,反复看,就慢慢的理解了.我其实不想说这种硬磨的方法有多好,我甚至不推荐,我建议大家阅读这本书时,由浅入深,有舍有得,先从宏观去理解去阅读,再慢慢深入,有条不紊的看下去.具体来说,当你看书的某一部分时,先看这部分的章节名,了解这部分这一章在讲什么,然后再看某一章,我拿"类文件结构"这一章来说,我必须

虚拟机字节码执行引擎

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

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

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

深入JAVA虚拟机之字节码执行引擎

前言:class文件结构.类加载机制.类加载器.运行时数据区这四个java技术体系中非常重要的知识,学习完了这些以后,我们知道一个类是通过类加载器加载到虚拟机,存储到运行时数据区,而且我们也知道了我们方法体内的代码被编译成字节码保存在方法表中的code属性中,那么虚拟机又是怎么执行这些代码的,得出方法输出结果的呢?这一节我们就要来学习,关于虚拟机字节码执行引擎的相关知识.通过这章节的学习,我们要掌握一下知识点: 1.运行时栈帧结构 2.方法调用 3.基于栈的字节码执行引擎 运行时栈帧结构 栈帧是