深入了解JVM——虚拟机字节码执行引擎

Posted 在咖啡里溺水的鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入了解JVM——虚拟机字节码执行引擎相关的知识,希望对你有一定的参考价值。

本文为 《深入理解Java虚拟机》第八章内容的学习笔记,部分内容经过二次加工。若对相关知识感兴趣,推荐购书深入阅读。若认为文章涉嫌侵权,请联系作者及时删除。
本作品采用 知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议 (CC BY-NC-SA 3.0 CN) 进行许可 。非商业性质转载请注明作者和出处,禁止商业性质转载。
开源创造世界

概述

执行引擎是Java虚拟机最核心的组成部分之一。

在Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的外观 Facade。从外观上看,所有的Java虚拟机执行引擎都是一致的:输入字节码文件,处理过程是字节码解析的等效过程,输出执行结果。

运行时栈帧结构

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

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

局部变量表

局部变量表 Local Variable Table 是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

局部变量不像类变量存在“准备阶段”,因此不会有初始的默认值。

操作数栈

操作数栈 Operand Stack 也常称为操作栈,是一个后入先出 Last In First Out LIFO 栈。操作数栈的每一个元素可以是任意的Java数据类型。

当一个方法刚开始执行的时候,方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。

动态连接

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

Class文件的常量池中存有大量的符号饮用,字节码中的方法调用指令就以常量池中指向方法的符号饮用作为参数。这些符号引用一部分会在类加载阶段活着第一次使用的时候就转化为直接饮用,这种转换称为静态解析。另外一部分将在每一次运行期间转化为直接饮用,这部分称为动态连接。

方法返回地址

方法被调用的位置

附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到帧栈之中。

实际开发中,一般把动态连接、方法返回地址与其他附加信息全部归为一类,成为栈帧信息。

方法调用

**方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定呗调用方法的版本(即调用哪一个方法),暂时还不设计方法内部的具体运行过程。

类需要在加载期间,甚至到运行期间才能确定目标方法的直接饮用。

解析

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

Java语言中符合编译器可知,运行期不可变要求的方法,包括静态方法和私有方法。

分派

静态分派

英文文档的称呼为 Method Overload Resolution,国内通常翻译为 静态分派。

A extends B,B称为静态类型 Static Type 外观类型 Apparent Type,A称为 实际类型 Actual Type。

编译器在重载时时通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译器可知的。因此,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。

所有以来静态类型来定位方法执行版本的分派动作称为静态分派。典型应用是方法重载。

静态分派发生在编译阶段。

解析与分派不是二选一的关系,他们是在不同层次上筛选、确定目标方法的过程。静态方法在类加载期进行解析,但静态方法也可以拥有重载版本,选择重载版本的过程是通过静态分派完成的。

动态分派

动态分派与重写相关。

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

单分派多分派

方法的接受者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。

Java语言的静态分派属于多分派类型。

Java语言的动态分派属于单分派类型。

虚拟机动态分派的实现

动态分派是非常频繁的动作,动态分派的版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正如此频繁的搜索。

最常用的“稳定优化”手段是为类在方法去中建立一个虚方法表 Virtual Method Table ,接口中存在接口方法表 Interface Method Table。使用虚方法表索引来代替元数据查找以提高性能。

在条件允许的情况下的“激进优化”方案:
- 内联缓存 Inline Cache
- 基于“类型继承关系分析” Class Hierarchy Analysis CHA 技术的守护内联 Guarded Inlining

动态类型语言的支持

动态类型语言

动态语言的关键特征是它的类型检查的主题过程是在运行期而不是编译期,常见的有:Erlang、Groovy、javascript、Lua、php、Python、Ruby。在编译期就进行类型检查过程的语言就是静态类型语言,如:Java、C++

java.lang.invoke

java.lang.invoke包的目的是在之前单出依靠符号饮用来确定调用的目标方法这种方式以外,提供一种新的动态确定目标方法的机制,称为Method Handle。

Method Handle 与 Reflection的区别:
- 虽然同为模拟方法调用,Reflection模拟Java代码层次的方法调用,MethodHandle是模拟字节码层次的方法调用。
- Reflection中的java.lang.reflect.Method对象包含的内容要比MethodHandle机制中的java.lang.invoke.MethodHandle包含的信息要多。MethodHandle仅仅包含与执行该方法相关的信息,相对而言较轻量级
- MethodHandle是对字节码的方法指令调用的模拟,理论上虚拟机在这方面做的各种优化,MethodHandle也可以采用类似思路支持。反射调用方法则不可以。
- Reflection API的设计目标只为Java语言服务,MethodHandle设计为可服务于所有Java虚拟机之上的语言。

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

解释执行

Java语言经常被人们定位为 解释执行 的语言,在JDK1.0中,这种定义相对准确。但是当主流的虚拟机中都包含了即时编译器之后,Class文件中的代码到底被解释执行还是编译执行,只有虚拟机自己才能准确判断。

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

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

基于栈的指令集主要优点是可移植,因为寄存器由硬件直接提供,与硬件相关。

栈架构指令集的主要缺点是执行速度相对来说稍慢一些。

以上是关于深入了解JVM——虚拟机字节码执行引擎的主要内容,如果未能解决你的问题,请参考以下文章

深入理解JVM学习笔记——-8虚拟机字节码执行引擎

深入理解JVM学习笔记——-8虚拟机字节码执行引擎

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

虚拟机字节码执行引擎

JVM | 第2部分:虚拟机执行子系统《深入理解 Java 虚拟机》

JVM系列之执行引擎