热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java虚拟机深入JVM字节码执行引擎

我们都知道,在当前的Java中(1.0)之后,编译器讲源代码转成字节码,那么字节码如何被执行的呢?

我们都知道,在当前的Java中(1.0)之后,编译器讲源代码转成字节码,那么字节码如何被执行的呢?这就涉及到了JVM的字节码执行引擎,执行引擎负责具体的代码调用及执行过程。就目前而言,所有的执行引擎的基本一致:


  1. 输入:字节码文件
  2. 处理:字节码解析
  3. 输出:执行结果。

物理机的执行引擎是由硬件实现的,和物理机的执行过程不同的是虚拟机的执行引擎由于自己实现的。




运行时候的栈结构

每一个线程都有一个栈,也就是前文中提到的虚拟机栈,栈中的基本元素我们称之为栈帧。栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。每个栈帧都包括了一下几部分:局部变量表、操作数栈、动态连接、方法的返回地址 和一些额外的附加信息。栈帧中需要多大的局部变量表和多深的操作数栈在编译代码的过程中已经完全确定,并写入到方法表的Code属性中。在活动的线程中,位于当前栈顶的栈帧才是有效的,称之为当前帧,与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令只针对当前栈帧进行操作。需要注意的是一个栈中能容纳的栈帧是受限,过深的方法调用可能会导致StackOverFlowError,当然,我们可以认为设置栈的大小。其模型示意图大体如下: 
运行时栈结构



针对上面的栈结构,我们重点解释一下局部变量表,操作栈,指令计数器几个概念:


1、局部变量表

是变量值的存储空间,由方法参数和方法内部定义的局部变量组成,其容量用Slot1作为最小单位。在编译期间,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。如果是实例方法,那局部变量表第0位索引的Slot存储的是方法所属对象实例的引用,因此在方法内可以通过关键字this来访问到这个隐含的参数。其余的参数按照参数表顺序排列,参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。举例说明:

public void test(){call(2,3);...call2(2,3);
}public void call(int i,int j){int b=2;...
}public static void call2(int i,int j){int b=2;...
}

为了方便起见,假设以上两段代码在同一个类中。这时call()所对应的栈帧中的局部变量表大体如下: 
实例方法局部变量表 
而call2()所对应的栈帧的局部变量表大体如下: 
类方法局部变量表




2、操作数栈

后入先出栈,由字节码指令往栈中存数据和取数据,栈中的任何一个元素都是可以任意的Java数据类型。和局部变量类似,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数中写入和提取内容,也就是出栈/入栈操作。操作数栈中元素的数据类型必须与字节码指令的序列严格匹配2,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。另外我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。




3、动态连接

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




4、方法返回地址

存放调用调用该方法的pc计数器的值。当一个方法开始之后,只有两种方式可以退出这个方法:1、执行引擎遇到任意一个方法返回的字节码指令,也就是所谓的正常完成出口。2、在方法执行的过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种方式成为异常完成出口。正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。 
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置,方法正常退出时,调用者的pc计数器的值作为返回地址,而通过异常退出的,返回地址是要通过异常处理器表来确定,栈帧中一般不会保存这部分信息。本质上,方法的退出就是当前栈帧出栈的过程。




方法调用

方法调用的主要任务就是确定被调用方法的版本(即调用哪一个方法),该过程不涉及方法具体的运行过程。按照调用方式共分为两类:


  1. 解析调用是静态的过程,在编译期间就完全确定目标方法。
  2. 分派调用即可能是静态,也可能是动态的,根据分派标准可以分为单分派和多分派。两两组合有形成了静态单分派、静态多分派、动态单分派、动态多分派

解析

在Class文件中,所有方法调用中的目标方法都是常量池中的符号引用,在类加载的解析阶段,会将一部分符号引用转为直接引用,也就是在编译阶段就能够确定唯一的目标方法,这类方法的调用成为解析调用。此类方法主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可访问,因此决定了他们都不可能通过继承或者别的方式重写该方法,符合这两类的方法主要有以下几种:静态方法、私有方法、实例构造器、父类方法。虚拟机中提供了以下几条方法调用指令:


  1. invokestatic:调用静态方法,解析阶段确定唯一方法版本
  2. invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
  3. invokevirtual:调用所有虚方法
  4. invokeinterface:调用接口方法
  5. invokedynamic:动态解析出需要调用的方法,然后执行

前四条指令固化在虚拟机内部,方法的调用执行不可认为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外[^footnote4])称为虚方法。


分派

分派调用更多的体现在多态上。


  1. 静态分派:所有依赖静态类型3来定位方法执行版本的分派成为静态分派,发生在编译阶段,典型应用是方法重载
  2. 动态分派:在运行期间根据实际类型4来确定方法执行版本的分派成为动态分派,发生在程序运行期间,典型的应用是方法的重写
  3. 单分派:根据一个宗量5 对目标方法进行选择。
  4. 多分派:根据多于一个宗量对目标方法进行选择。

JVM实现动态分派

动态分派在Java中被大量使用,使用频率及其高,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率,因此JVM在类的方法区中建立虚方法表(virtual method table)来提高性能。每个类中都有一个虚方法表,表中存放着各个方法的实际入口。如果某个方法在子类中没有被重写,那子类的虚方法表中该方法的地址入口和父类该方法的地址入口一样,即子类的方法入口指向父类的方法入口。如果子类重写父类的方法,那么子类的虚方法表中该方法的实际入口将会被替换为指向子类实现版本的入口地址。 
那么虚方法表什么时候被创建?虚方法表会在类加载的连接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。




方法的执行


解释执行

在jdk 1.0时代,Java虚拟机完全是解释执行的,随着技术的发展,现在主流的虚拟机中大都包含了即时编译器(JIT)。因此,虚拟机在执行代码过程中,到底是解释执行还是编译执行,只有它自己才能准确判断了,但是无论什么虚拟机,其原理基本符合现代经典的编译原理,如下图所示: 
此处输入图片的描述 
在Java中,javac编译器完成了词法分析、语法分析以及抽象语法树的过程,最终遍历语法树生成线性字节码指令流的过程,此过程发生在虚拟机外部。


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

Java编译器输入的指令流基本上是一种基于的指令集架构,指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。另外一种指令集架构则是基于寄存器的指令集架构,典型的应用是x86的二进制指令集,比如传统的PC以及Android的Davlik虚拟机。两者之间最直接的区别是,基于栈的指令集架构不需要硬件的支持,而基于寄存器的指令集架构则完全依赖硬件,这意味基于寄存器的指令集架构执行效率更高,单可移植性差,而基于栈的指令集架构的移植性更高,但执行效率相对较慢,初次之外,相同的操作,基于栈的指令集往往需要更多的指令,比如同样执行2+3这种逻辑操作,其指令分别如下: 
基于栈的计算流程(以Java虚拟机为例):

iconst_2 //常量2入栈
istore_1
iconst_3 //常量3入栈
istore_2
iload_1
iload_2
iadd //常量2、3出栈,执行相加
istore_0 //结果5入栈

而基于寄存器的计算流程:

mov eax,2 //将eax寄存器的值设为1
add eax,3 //使eax寄存器的值加3



基于栈的代码执行示例

下面我们用简单的案例来解释一下JVM代码执行的过程,代码实例如下:


public class MainTest {public static int add(){int result=0;int i=2;int j=3;int c=5;return result =(i+j)*c;}public static void main(String[] args) {MainTest.add();}
}

使用javap指令查看字节码:

{public MainTest();flags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."":()V4: returnLineNumberTable:line 2: 0public static int add();flags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=0 //栈深度2,局部变量4个,参数0个0: iconst_0 //对应result=0,0入栈1: istore_0 //取出栈顶元素0,将其存放在第0个局部变量solt中2: iconst_2 //对应i=2,2入栈3: istore_1 //取出栈顶元素2,将其存放在第1个局部变量solt中4: iconst_3 //对应 j=3,3入栈5: istore_2 //取出栈顶元素3,将其存放在第2个局部变量solt中6: iconst_5 //对应c=5,5入栈7: istore_3 //取出栈顶元素,将其存放在第3个局部变量solt中8: iload_1 //将局部变量表的第一个slot中的数值2复制到栈顶9: iload_2 //将局部变量表中的第二个slot中的数值3复制到栈顶10: iadd //两个栈顶元素2,3出栈,执行相加,将结果5重新入栈11: iload_3 //将局部变量表中的第三个slot中的数字5复制到栈顶12: imul //两个栈顶元素出栈5,5出栈,执行相乘,然后入栈13: dup //复制栈顶元素25,并将复制值压入栈顶.14: istore_0 //取出栈顶元素25,将其存放在第0个局部变量solt中15: ireturn //将栈顶元素25返回给它的调用者LineNumberTable:line 4: 0line 5: 2line 6: 4line 7: 6line 8: 8public static void main(java.lang.String[]);flags: ACC_PUBLIC, ACC_STATICCode:stack=1, locals=1, args_size=10: invokestatic #2 // Method add:()I3: pop4: returnLineNumberTable:line 12: 0line 13: 4
}

执行过程中代码、操作数栈和局部变量表的变化情况如下: 
指令0执行

指令1执行

指令2执行

指令3执行

指令4执行

指令5执行

指令6执行

指令7执行

指令8执行

指令9执行

指令10执行

指令11执行

指令12执行

指令13执行

指令14执行

指令15执行




  1. 也成为容量槽,虚拟规范中并没有规定一个Slot应该占据多大的内存空间。 ↩
  2. 这里的严格匹配指的是字节码操作的栈中的实际元素类型必须要字节码规定的元素类型一致。比如iadd指令规定操作两个整形数据,那么在操作栈中的实际元素的时候,栈中的两个元素也必须是整形。 ↩
  3. Animal dog=new Dog();其中的Animal我们称之为静态类型,而Dog称之为动态类型。两者都可以发生变化,区别在于静态类型只在使用时发生变化,变量本身的静态类型不会被改变,最终的静态类型是在编译期间可知的,而实际类型则是在运行期才可确定。 ↩
  4. Animal dog=new Dog();其中的Animal我们称之为静态类型,而Dog称之为动态类型。两者都可以发生变化,区别在于静态类型只在使用时发生变化,变量本身的静态类型不会被改变,最终的静态类型是在编译期间可知的,而实际类型则是在运行期才可确定。 ↩
  5. 宗量:方法的接受者与方法的参数称为方法的宗量。 
    举个例子: 
    public void dispatcher(){ 
    int result=this.execute(8,9); 

    public void execute(int pointX,pointY){ 
    //TODO 


    在dispatcher()方法中调用了execute(8,9),那此时的方法接受者为当前this指向的对象,8、9为方法的参数,this对象和参数就是我们所说的宗量。 ↩

推荐阅读
  • Java中不同类型的常量池(字符串常量池、Class常量池和运行时常量池)的对比与关联分析
    在研究Java虚拟机的过程中,笔者发现存在多种类型的常量池,包括字符串常量池、Class常量池和运行时常量池。通过查阅CSDN、博客园等相关资料,对这些常量池的特性、用途及其相互关系进行了详细探讨。本文将深入分析这三种常量池的差异与联系,帮助读者更好地理解Java虚拟机的内部机制。 ... [详细]
  • 本文详细介绍了Java反射机制的基本概念、获取Class对象的方法、反射的主要功能及其在实际开发中的应用。通过具体示例,帮助读者更好地理解和使用Java反射。 ... [详细]
  • 本文详细解析了Java类加载系统的父子委托机制。在Java程序中,.java源代码文件编译后会生成对应的.class字节码文件,这些字节码文件需要通过类加载器(ClassLoader)进行加载。ClassLoader采用双亲委派模型,确保类的加载过程既高效又安全,避免了类的重复加载和潜在的安全风险。该机制在Java虚拟机中扮演着至关重要的角色,确保了类加载的一致性和可靠性。 ... [详细]
  • Presto:高效即席查询引擎的深度解析与应用
    本文深入解析了Presto这一高效的即席查询引擎,详细探讨了其架构设计及其优缺点。Presto通过内存到内存的数据处理方式,显著提升了查询性能,相比传统的MapReduce查询,不仅减少了数据传输的延迟,还提高了查询的准确性和效率。然而,Presto在大规模数据处理和容错机制方面仍存在一定的局限性。本文还介绍了Presto在实际应用中的多种场景,展示了其在大数据分析领域的强大潜力。 ... [详细]
  • 本指南从零开始介绍Scala编程语言的基础知识,重点讲解了Scala解释器REPL(读取-求值-打印-循环)的使用方法。REPL是Scala开发中的重要工具,能够帮助初学者快速理解和实践Scala的基本语法和特性。通过详细的示例和练习,读者将能够熟练掌握Scala的基础概念和编程技巧。 ... [详细]
  • Spring – Bean Life Cycle
    Spring – Bean Life Cycle ... [详细]
  • Python 数据可视化实战指南
    本文详细介绍如何使用 Python 进行数据可视化,涵盖从环境搭建到具体实例的全过程。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 本文介绍如何使用 Python 的 DOM 和 SAX 方法解析 XML 文件,并通过示例展示了如何动态创建数据库表和处理大量数据的实时插入。 ... [详细]
  • 使用Java 1.8 编译兼容1.6 JVM 的Class文件
    本文探讨了如何使用Java 1.8 编译器生成能够在1.6 JVM 上运行的Class文件,并介绍了Spring框架中的相关配置。 ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 浏览器作为我们日常不可或缺的软件工具,其背后的运作机制却鲜为人知。本文将深入探讨浏览器内核及其版本的演变历程,帮助读者更好地理解这一关键技术组件,揭示其内部运作的奥秘。 ... [详细]
  • 深入解析Java虚拟机的内存分区与管理机制
    Java虚拟机的内存分区与管理机制复杂且精细。其中,某些内存区域在虚拟机启动时即创建并持续存在,而另一些则随用户线程的生命周期动态创建和销毁。例如,每个线程都拥有一个独立的程序计数器,确保线程切换后能够准确恢复到之前的执行位置。这种设计不仅提高了多线程环境下的执行效率,还增强了系统的稳定性和可靠性。 ... [详细]
  • 线程能否先以安全方式获取对象,再进行非安全发布? ... [详细]
author-avatar
红Lisa
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有