欢迎您光临本小站。希望您在这里可以找到自己想要的信息。。。

深入理解java虚拟机(六)

java water 1986℃ 0评论

虚拟机字节码执行引擎

代码编译的结果是从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

概述

执行引擎是Java虚拟机最核心的组成部分之一。“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎则是由自己实现的,因此可以自己自行指定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式

Java虚拟机规范中指定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观(Facade)。在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能有解释执行(通过解释器执行)和编译执行(通过即时编译产本地代码执行)两种选择。所有Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果

运行时栈帧结构

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

每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,仅仅取决于具体的虚拟机实现

一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。

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

局部变量表:是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(Slot)为最小单位

操作数栈:也常被称为操作栈,它是一个后入先出栈。当一个方法开始执行时,这个方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令向操作数栈中写入和提取内容。

在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。

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

方法返回地址:第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,这种方式称为正常完成出口;另一种退出方式是,在方法执行过程中遇到异常,并且这个异常没有在方法体内得到处理,这种退出方式称为异常完成出口;无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法退出的过程实际上等同于在当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中。调整PC计数器的值以指向方法调用指令后面的一条指令等。

方法调用

方法调用并不等同于方法执行,方法调用阶段唯一任务就是确定被调用(方法的版本),暂时还不涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普遍、最频繁的操作,但Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前所说的直接引用)。给Java带来了更强大的动态扩展能力,也是Java方法的调用过程变得相对于复杂起来,需要在类加载期间甚至到运行期间才能确定本方法的直接引用。

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

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

解析调用一定是个静态的过程,在编译期间就完全确定,在类装载额解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。完分派调用则可能是静态的也可能是动态的,根据分配依据的宗量数可分为单分派和多分派。这两类分派方式两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派四种分派情况。

分派:分派调用过程将会揭示多态性特征的一些最基本的体现(重载和重写)

Human man = new Man() 我们把上面代码中的“Human”称为变量的静态类型或者外观类型,后面的“Man”则称为变量的实际类型,静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

静态分派:所有依赖静态类型来定位方法执行版本的分派动作,都称为静态分派。静态分派最典型应用就是方法重载。静态分派发生在编译阶段。编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一的”,往往只能确定一个“更加合适的”版本。

解析与分派这两者之间额关系并不是二选一的排他关系,它们是在不同层次上去筛选和确定目标方法的过程

动态分派:它和多态性的另外一个重要体现,重写(Overrider)有着密切的关联

Java虚拟机是如何根据实际类型来分派方法执行版本的呢?

在运行期根据实际类型确定方法执行版本的分派过程称为动态分派

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

静态分派属于多分派类型

动态分派属于单分派类型

JDK1.6时期的Java语言是一门静态多分派、动态单分派的语言。

虚拟机动态分派的实现

动态分派是非常频繁的动作,动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索最合适的目标方法,虚拟机基于性能的考虑,大部分实现都不会真的进行如此频繁的搜索,优化手段是为类的方法去中建立一个虚方法包(vtable,invokeinterface执行时会用到接口方法表-Interface Method Table简称(itable,使用虚方法表索引来代替元数据查找以提高性能。

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

方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该方法表也初始化完毕。

方法表是分派调用的“稳定优化”手段,虚拟机除了使用方法表之外,在条件运行的情况,还会使用“内联缓存”和基于“类型继承关系分析”技术的守护内联两种非稳定的“激进优化”手段来获得更高的性能

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

Java虚拟机的执行引擎在执行Java代码的时候都有解释执行和编译执行两种选择。

解释执行:不论是解释还是编译,也不论是物理机还是虚拟机,对于应用程序,机器都不可能如人那样阅读和理解,然后就获得了执行能力。大部分的程序代码到物理机的目标代码或虚拟机能执行的指令集之前都需要许多步骤。

基于物理机、Java虚拟机或者是非Java的其他高级语言虚拟机的语言,大多都遵循这种基于现代经典编译原理的思路,在执行钱先对程序源码进行词法分析和语法分析处理,把源码转化为抽象语法树。

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

Java编译器输出的指令集,基本上是一种基于栈的指令集架构,指令流里面的指令大部分都是零地址指令,它们依赖操作数栈进行工作。

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

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

栈架构指令集的主要缺点是执行速度相对来说稍慢一些。所有主流物理机的指令集都是寄存器架构。

栈架构指令集代码虽然紧凑,但完成相同功能所需的指令数量一般会比寄存器架构多,因为出栈、入栈操作本身就会产生了相当多的指令。更重要的是栈实现在内存之中,频繁的栈访问也就意味着频繁的内存访问,相对于处理器来说,内存始终是执行速度的瓶颈。

基于栈的解释器执行过程:虚拟机中解释器和即时编译器都会对输入的字节码进行优化。

转载请注明:学时网 » 深入理解java虚拟机(六)

喜欢 (0)or分享 (0)

您必须 登录 才能发表评论!