• 5.3.10 执行引擎


    任何Java虚拟机实现的核心都是它的执行引擎。在Java虚拟机规范中,执行引擎的行为使用 指令集来定义。对于每条指令,规范都详细规定了当实现执行到该指令时应该处理什么,但是却对如何处理言之甚少。在前面的章节中提到过,实现的设计者有权决定如何执行字节码:实 现可以采取解释、即时编译或直接用芯片上的指令执行,还可以是它们的混合,或任何你能想 到的新技术。

    和本章幵头提到的对“Java虚拟机”这个术语有三种不同的理解一样,“执行引擎”这个术 语也可以有三种理解:一个是抽象的规范,一个是具体的实现,另一个是正在运行的实例,抽象规范使用指令集规定了执行引擎的行为。具体实现可能使用多种不同的技术——包括软件方 面、硬件方面或数种技术的集合。作为运行时实例的执行引擎就是一个线程。

    运行中Java程序的每一个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,它要么在执行字节码,要么在执行本地方法。一个线程可能通过解释或者使用芯片 级指令直接执行字节码,或者间接通过即时编译器执行编译过的本地代码。Java虚拟机的实现可 用一些对用户程序不可见的线程,比如垃圾收集器。这样的线程不需要是实现的执行引擎的

    实例。所有属于用户运行程序的线程,都是在实际工作的执行引擎。

    指令集方法的字节码流是由Java虚拟机的指令序列构成的,每一条指令包含一个单字节的操作码,后面跟随0个或多个操作数。操作码表明需要执行的操作;操作数向java虚拟机提供执行操作码需要的额外信息。操作码本身就已经规定了它是否需要跟随操作数,以及如果有操作 数的话,它是什么形式的。很多Java虚拟机的指令不包含操作数,仅仅是由一个操作码字节构成 的。根据操作码的需要,虚拟机可能除了跟随操作码的操作数之外,还需要从另外一些存储区域得到操作数。当虚拟机执行一条指令的时候,可能使用当前常量池中的项、当前帧的局部变 量中的值,或者位于当前帧操作数栈顶端的值。


    抽象的执行引擎每次执行一条字节码指令。Java虚拟机中运行的程序的每个线程(执行引擎 实例)都执行这个操作。执行引擎取得操作码,如果操作码有操作数,取得它的操作数。它执 行操作码和跟随的操作数规定的动作,然后再取得下一个操作码。这个执行字节码的过程在线 程完成前将一直持续,通过从它的初始方法返回,或者没有捕获抛出的异常都可以标志着线程 的完成。

    执行引擎会不时遇到请求本地方法调用的指令。在这个时候,虚拟机负责试着发起这个本 地方法调用。如果本地方法返回了(假设是正常返回,而不是抛出了一个异常),执行引擎会继 续执行字节码流中的下一条指令。

    可以这样来看,本地方法是Java虚拟机指令集的一种可编程扩展。如果一条指令请求一个对 本地方法的调用,执行引擎就会调用这个本地方法。运行这个本地方法就是Java虚拟机对这条指 令的执行。当本地方法返回了,虚拟机继续执行下一条指令。如果本地方法异常中止了(抛出 了一个异常),虚拟机就按照好比是这条指令拋出这个异常一样的步骤来处理这个异常。

    执行一条指令包含的任务之一就是决定下一条要执行的是什么指令。执行引擎决定下一个 操作码时有三种方法。很多指令的下一个操作码就是在当前操作码和操作数之后紧跟的那个字 节(如果字节码流里面还有的话)。另外一些指令,比如goto或者return,执行引擎决定下一个操 作码时把它当做当前执行指令的一部分。假若一条指令抛出了一个异常,那么执行引擎将搜索 合适的catch子句,以决定下一个将执行的操作码是什么。

    有些指令可以抛出异常。比如,athrow指令,就明确地抛出一个异常。这条指令就是Java源 代码中的throw语句的编译后形式。每当执行一条athrow指令的时候,它都将抛出一个异常。其他 抛出异常的指令都只有在满足某些特定条件的时候才抛出异常。比如,假若Java虚拟机发现程序 试图用0除一个整数,它就会抛出一个ArithmeticException异常。这只有在执行四条特定的除法指 令(idev、ldiv、irem和丨rem )的时候,或者计算int或者long的余数的时候,才可能发生。

    Java虚拟机指令集的每种操作码都有助记符。使用典型的汇编语言风格,iava字节码流可以 用助记符跟着(可选的)操作数值来表示。

    法的字节码流和助记符的例子如下所示,考虑这个类里的doMathForever ()方法:

    doMathForever ()方法的字节码流可以被反汇编成下面的助记符。Java虚拟机规范中没有定 义正式的表示方法字节码的助记符的语法。下面显示的代码说明了本书所采用的用助记符表示字 节码的方式。左边的列表示每条指令开始时从字节码的开头开始算起的每条指令的字节偏移量;中间的列表示指令和它的操作数;右边的列包含注释,用双斜杠隔开,如同Java源代码的格式。

    0 iconst_0 // 03
    1 istore_0 // 3b
    2 iinc 0, 1 // 84 00 01
    5 iload_0 // la
    6 iconst_2 // 05
    7 imul // 68
    8 istore_0 // 3b
    9 goto 2 // a7 ff f9
    这种表示助记符的方式和Sun的Java 2 SDK里的javap程序的输出很相似。使用javap可以査看任何class文件中方法的字节码助记符。请注意跳转地址是按照从方法起始开始算起的偏移量 来给出的。goto指令导致虚拟机跳转到从方法起始计算的位于偏移量2的指令。实际上操作数是 负7。要执行这条指令,虚拟机在当前PC寄存器的内容上加上这个操作数。结果就是iinc指令的 地址:偏移量2。为了让助记符更加易读,所看到的这条跳转指令后面的操作数已经是经过计算 后的结果了。助记符显示的是“goto 2”,而不是“goto-7”。

    Java虚拟机指令集关注的中心是操作数栈。一般是把将要使用的值会压人桟中。虽然Java虚 拟机没有保存任意值的寄存器,但每个方法都有一个局部变量集合。指令集实际的工作方式就 是把局部变量当做寄存器,用索引来访问。不过,不同于iinc指令——它可以直接增加一个局部 变量的值,要使用保存在局部变童中的值之前,必须先将它从压入操作数栈。

    举例来说,用一个局部变量除另外一个,虚拟机必须把它们都压入栈,执行除法,然后把 结果重新保存到局部变量。要把数组元素或对象的字段保存到局部变量中,虚拟机必须先把值 压人栈,然后保存到局部变量中去。要把保存在局部变量中的值陚于数组元素或者对象字段, 虚拟机必须按照相反的步骤操作。它首先必须把局部变量的值压人栈,然后从栈中弹出,再放 于堆上的数组元素或对象字段中。

    Java虚拟机指令集的设计遵循几个不同的目标,但它们之间是有冲突的。这几个目标就是本 书的前面所描述的整个Java体系结构的目的所在:平台无关性、网络移动性以及安全性。

    平台无关性是影响指令集设计的最大因素。指令集的这种以找为中心、而非以寄存器为中 心的设计方法,使得在那些只有很少的寄存器,或者寄存器很没有规律的机器上实现java更便利, Intel 80X86就是一个例子。由于指令集具有这种以栈为中心的特征,所以在很多平台体系结构 上都很容易实现Java虚拟机。

    Java以栈为中心设计指令集的另一个动机是,编译器一般采用以栈为基础的结构向连接器或 优化器传递编译的中间结果。Java class文件在很多方面都和C编译器产生的.o文件(UNIX)或 者.obj文件(Windows)很相似,实际上它表示了一种Java程序的中间编译结果形式。对于java的情况来,虚拟机是作为(动态)连接器使用的,也可以作为优化器。在Java虚拟机指令集设计 中,以栈为中心的体系结构可以将运行时进行的优化工作与执行即时编译或者自适应优化的执 行引擎结合起来。

    在第4章中讲过,设计中一个主要考虑因素是class文件的紧凑性。紧凑性对于提高在网络上 传递class文件的速度是很重要的。在class文件中保存的宇节码,除了两个处理表跳转的指令之 外,都是按照字节对齐的。操作码的总数很小,所以操作码可以只占据一个字节。这种设计策 略有助于class文件的紧凑,但却是以可能影响程序运行的性能为代价的。某些Java虚拟机实现, 特别是那些在芯片上执行字节码的实现,单字节的操作码可能使得一些可以提高性能的优化无 法实现。同样,假若字节码流是以字对齐而非字节对齐的话,某些实现可能会得到更好的性能。 (实现可以重新对齐字节码流,或者在装载类的时候把操作码转换成更加有效的形式。字节码在 class文件中是按字节对齐的,在抽象方法区和执行引擎的规范中也是这么规定的。不同的具体 实现可以用它们喜欢的任何形式保存装载后的字节码流。)

    指导指令集设计的另一个目标就是进行字节码验证的能力,特别是使用数据流分析器进行的一次性验证。Java的安全框架需要这种验证能力。在装载字节码的时候使用数据流分析器迸行 —次性验证,而非在执行每条指令的时候进行验证,这样做有助于提高执行速度。在指令集中 体现这个目标的表现之一,就是绝大部分操作码都指明了它们需要操作的类型。

    比如说,不是简单地采用一条指令(该指令从操作数栈中取出一个字并保存到局部变量中), Java虚拟机的指令集而是采用两条指令。—条指令是istore—弹出并保存int类型;另一条指令是fstore-弹出并保存float类型。在执行的时候这两条指令所完成的功能是完全一致的:弹出一个字并保存。要区分弹出并保存的到底是int类型还是float类型,只对验证过程有重要作用。

    对于某些指令,虚拟机需要知道被操作的类型,以决定如何执行操作。比如,Java虚拟机支持两种把两个字加起来并得到一个结果字的操作。一种是把字当做int处理,另一种是当做float处理。这两条指令的区别在于方便验证,同时也告诉虚拟机需要的是整数操作还是浮点数操作。

    有一些指令可以操作任何类型。比如说dup指令不管栈顶的字是什么类型都可以复制它。还 有一些指令不操作有类型的值,比如goto。但是大部分指令都操作特定的类型。这种“有类型“的指令,可以使用助记符通过一个字符前缀来表明它们操作的类型。表5-2列举了不同类型的前缀。有一些指令不包含前缀。比如arraylengih或instanceof,因为它们的类型是再明显不过的。 arraylength操作码需要一个数组引用。instanceof 操作码需要一个对象引用。
    表5-2字节码助记符的前缀
    类型 代码 示例 描述
    byte b baload 从数组装载byte类型
    short s saload 从数组装栽short类型
    int i aload 从数组装载int类型
    long 1 laload 从数组装栽long类型
    char c caload 从数组装载char类型
    float f faload 从数组装载float类型
    double d daload 从数组装載double类型
    reference a aaload 从数组装载引用类型

    操作数找中的数值必须按照适合它们类型的方式使用。比如说压人4个int,但却把它们当作 两个long来做加法,这是非法的。把一个float值从局部变量压入操作数栈,然后把它作为int保存 到堆中的数组中去,这也是非法的。从一个位于堆中的对象字段压人一个double值,然后把栈中 最顶端的两个字作为类型引用保存到局部变量,这也是非法的。Java编译器所坚持的强类型规则 对Java虚拟机实现同样也是适用的。

    当执行那些与类型无关的一般性栈操作指令时,实现也必须遵守一些规则。前面讲过,不 管是什么类型,dup指令压入栈中顶端那个字的拷贝。这条指令可以用在任何占据一个字的值类 型上,如int、float、引用或return Address。但是,如果栈顶包含的是long或者double类型,它们 占据了两个连续的栈空间,这时使用dup就是非法的。位于栈顶的long或者double需要用dup2指 令复制两个字,在操作数栈中压入栈顶的两个字的拷贝。一般性指令不能用来切割双字值。

    为了使指令集足够小,用单字节表示每一个操作码,但并不是在所有类型上都支持所有的 操作。很多操作对byte、short和char都不支持。这些类型在从堆或者方法区转移到栈帧的时候被 转换成int,它们被当作int来进行操作,然后在操作完成后重新保存到堆或方法去的时候再转换 short或者char。


    线程Java虚拟机规范定义了线程模型,这个模型的目标是要有助于在很多体系结构上都实 现它。Java线程模型的一个目标就是使实现的设计者,在可能的情况下使用本地线程。否则,设 计者可以在它们的虚拟机实现内部实现线程机制。在一台多处理器的主机上使用本地线程的好 处就是,Java程序不同的线程可以在不同的处理器上并行工作。

    Java线程模型的折中之一就是优先级的规范考虑最小公分母问题。Java线程可以运行于10个 优先级中的任何一个。级别1是优先级最低的,而级别10是最高的。如果设计者使用本地线程, 他可以用合适的方法把10个Java优先级映射到机器本地的优先级上。Java虚拟机规范对于不同优 先级别的线程行为,只规定了所有最高级别的线程会得到大多数的CPU时间,较低优先级别的 线程只有在所有比它优先级更高的线程全都阻塞的情况下才能保证得到CPU时间。级別低的线 程在级别髙的线程没有被阻塞的时候也可能得到CPU时间,但是这没有任何保证。

    规范没有假设不同优先级的线程采用时间分片方式。因为并不是所有的体系结构都采用时 间片(在这里,时间分片的含义是:就算没有线程被阻塞,所有优先级的所有线程都会保证得 到一些CPU时间)。就算在那些采用时间片的体系结构上,用来分配时间片给不同优先级线程的算法也存在非常大的差异。


    第2章中讲到,程序的正确运行不能依靠时间分片。只有在向Java虚拟机给出提示,某个线 程应该比其他线程使用更多的时间,这时候才使用线程优先级。要协调多线程之间的活动,应 该使用同步。

    任何Java一虚拟机的线程实现都必须支持同步的两个方面:对象锁定,线程等待和通知。对象锁定使独立运行的线程访问共享数据的时候互斥。线程等待和通知使得线程为了达到同一个目 标而互相协同工作。运行中的程序通过Java虚拟机指令集来访问上锁机制,还通过Object类的 wait ()方法、notify ()方法和notifyAll ()方法来访问线程等待和通知机制。更多的细节请 参阅第20章。

    在Java虚拟机规范中,Java线程的行为是通过术语一变量、主存和工作内存一来定义的。 每一个Java虚拟机实例都有一个主存,用于保存所有的程序变量(对象的实例变量、数组的元 素以及类变量)。每一个线程都有一个工作内存,线程用它保存所使用和賦值的变量的“工作拷贝“。

    基本上,管理低层线程行为的规则,规定了一个线程何时可以做及何时必须做以下的事情:
    1)把变量的值从主存拷贝到它的工作内存。

    2)把值从它的工作内存写回到主存。

    在特定条件下,规则指定了精确的和可预言的读写内存的顺序。然而另外—些条件下,规则没有规定任何顺序。规则,是设计来让Java程序员利用可以预期的行为建立多线程程序,而给 实现的设计者更多的灵活性的。这种灵活性使Java虚拟机实现的设计者从标准硬件和软件技术中 得到好处,它们可以提高多线程程序的性能。

  • 相关阅读:
    Discourse 如何不使用 Let’s Encrypt 而使用 CA 签名的密钥进行安装
    Discourse 重复安装过程中的密钥签发问题
    Discourse 升级后提示 https 混合内容
    CentOS 8 安装 docker 报错 containerd.io >= 1.2.2-3
    MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
    培养自己的5项能力
    高效率工作方式
    项目的架构演进过程
    如何预防后台被攻击,且看Tomcat的安全配置
    redis的缓存更新策略,缓存粒度控制
  • 原文地址:https://www.cnblogs.com/mongotea/p/11979483.html
Copyright © 2020-2023  润新知