JVM 体系结构与工做方式

阅读本文大概须要 12.8 分钟。html

0. 前言

JVM 可以跨计算机体系机构来执行 Java 字节码,也就是咱们所说的 Java 能够跨平台执行,主要是因为 JVM 屏蔽了与各个计算机平台相关的软件或者硬件之间的差别,使得与平台相关的耦合统一由 JVM 提供者来实现。java

本文将介绍下面内容:缓存

  1. 介绍 JVM 的整体设计的体系结构
  2. 介绍 JVM 的执行引擎是如何工做的
  3. 介绍执行引擎如何模拟执行 JVM 指令

1. JVM 体系结构

在前面的章节,已经深刻介绍了 Class 类,而且简单了解如何经过类加载器将 Java 字节码加载到 JVM 中。下面来看看 JVM 的体系结构是如何设计的,这里从宏观的角度进行分析,让你们了解一下最基本的 JVM 结构和工做模式。bash

首先咱们思考下面这个问题网络

  • JVM 和实体机到底有何不一样?

1.1 何谓 JVM

JVM 的全称是 Java Virtual Machine,Java 虚拟机,它经过模拟一个计算机来达到一个计算机所具备的计算功能。数据结构

咱们先来看一下真实的计算机如何才能具有计算功能。架构

以计算为中心看计算机的体系结构能够分为如下几个部分:oracle

  • 指令集,计算机所能识别的机器语言的命令合集。
  • 计算单元,识别并控制指令执行的功能模块。
  • 寻址方式,地址的位数、最小地址和最大地址范围,以及地址的运行规则。
  • 寄存器定义,包括操做数寄存器、变址寄存器、控制寄存器等的定义、数量和使用方式。
  • 存储单元,可以存储操做数和保存操做结构的单元,如内核缓存、内存和磁盘等。

上面几个部分和咱们所说的代码执行最密切的仍是指令集,下面会详细介绍指令集是如何定义的。jvm

什么是指令集?有何做用?性能

指令集是在 CPU 中用来计算和控制计算机系统的一套指令的集合,每一种新型的 CPU 在设计时都规定了一系列与其余硬件电路相配合的指令系统。而指令集的先进与否也关系到 CPU 的性能发挥,是 CPU 性能的一个重要标志。

指令集和汇编语言有什么关系?

指令集是能够直接被机器识别的机器码,也就是必须以二进制格式存在于计算机中。

汇编语言是可以被人识别的指令,汇编语言在顺序和逻辑上是与机器指令一一对应的。也就是说,汇编语言是为了让人可以更容易地记住机器指令的助记符。

指令集和 CPU 架构有何关系?不一样的 CPU 指令集是否兼容?CPU 的架构是否会影响指令集?

汇编语言中都是对寄存器和段的直接操做的命令,寄存器和段等芯片都是架构的一部分,因此不一样的芯片架构设计必定会对应到不一样的机器指令集合。可是不一样的芯片厂商每每都会采用兼容的方式来兼容其余不一样架构的指令集,由于垄断操做系统的微软,操做系统是管理计算的的真正入口,几乎全部程序都要通过操做系统的调用,若是操做系统不支持某种芯片的指令集,用户的程序是不可能执行的。

如何查看 CPU 支持不一样的指令集?

经过 cpu-z 软件来查看 CPU 支持哪些指令集,看下图:

在指令集这行咱们能看到,当前的 CPU 支持 11 种指令集。


说完了指令集,咱们回到 JVM 主题,想一下咱们在前面提出的问题 JVM 和实体机到底有何不一样?

大致有以下几点:

  • 一个抽象规范,这个规范就约束了 JVM 究竟是什么,他有哪些组成部分,这些抽象的规范都在 The JavaTM Virtual Machine Specification 中详细描述了。
  • 一个具体的实现,所谓具体的实现就是不一样的厂商按照这个抽象的规范用软件或者软件和硬件结合的方式在相同或者不一样的平台上的具体的实现。
  • 一个运行的实例,当用其运行一个 Java 程序时,它就是一个运行中的实例,每一个运行中的 Java 程序都是一个 JVM 实例。

JVM 和实体机同样也必须有一套合适的指令集,这个指令集可以被 JVM 解析执行,这个指令集咱们称为 JVM 字节码指令集,符合 class 文件规范的字节码均可以被 JVM 执行

1.2 JVM 体系结构详解

下面咱们再看看除了指令集以外,JVM 还须要哪些组成部分。以下图所示,JVM 的结构基本上只由 4 部分组成。

  • 类加载器,在 JVM 启动时或者在类运行时将须要的 class 加载到 JVM 中。
  • 执行引擎,执行引擎的任务时负责执行 class 文件中包含的字节码执行,至关于实际机器的 CPU。
  • 内存区,将内存划分红若干个区以模拟实际机器上的存储、记录和调度功能模块,如实际机器上的各类功能的寄存器或者 PC 指针的记录器等。
  • 本地方法调用,调用 C 或 C++ 实现的本地方法的代码返回结果。

那详细描述下这 4 个部分:

  1. 类加载器:

    ClassLoader 工做机制会在后面详细写一篇文章介绍。这里须要说明的是,每一个被 JVM 装载的类型都有一个对应的 java.lang.Class 类的实例来表示该类型,该实例能够惟一表示被 JVM 装载的 Class 类,这个实例和其余类的实例同样存放在 Java 的堆中

  2. 执行引擎:

    执行引擎是 JVM 的核心部分,执行引擎的做用就是解析 JVM 字节码指令,获得执行结果。在《Java 虚拟机规范》中详细定义了执行引擎遇到每条字节码指令时应该处理什么,而且获得什么结果。可是并无规定执行引擎应该如何或采起什么方式处理而获得这个结果。由于执行引擎具体采起什么方式由 JVM 的实现厂家去实现,是直接解释执行仍是采用 JIT(just-in-time 即便编译) 技术转成本地代码去执行,仍是采用寄存器这个芯片模式去执行均可以。因此执行引擎的具体实现有很大的发挥空间,如 SUN 的 hotspot 是基于栈的执行引擎,而 Google 的 Dalvik 是基于寄存器的执行引擎。

    执行引擎也就是执行一条条代码的一个流程,而代码都是包含在方法体内的,因此执行引擎本质上就是执行一个个方法所串起来的流程,对应到操做系统中一个执行流程是一个 Java 线程,Java 进程能够有多个同时执行的执行流程。这样说,每一个 Java 线程就是一个执行引擎的实例,那么在一个 JVM 实例中就会同时有多个执行引擎在工做,这些执行引擎有的在执行用户的程序,有的在执行 JVM 内部的程序(如 Java 垃圾收集器)。

  3. Java 内存管理

    执行引擎在执行一段程序时须要存储一些东西,如操做码须要的操做数,操做码的执行结果须要保存。class 类的字节码还有类的对象等信息都须要在执行引擎执行以前就准备好。从最开始的图中,能够看出一个 JVM 实例会有一个方法区、Java 堆、Java 栈、PC 寄存器和本地方法区。其中方法区和 Java 堆是全部线程共享的,也就是能够被全部的执行引擎实例访问。每一个新的执行引擎实例被建立时会为这个执行引擎建立一个 Java 栈和一个 PC 寄存器,若是当前正在执行一个 Java 方法,那么在当前的这个 Java 栈中保存的是该线程中方法调用的状态,包括方法的参数、方法的局部变量、方法的返回值以及运算符中间的结果等。而 PC 寄存器会指向即将执行的下一条指令。

  1. 本地方法调用

    本地方法调用,则存储在本地方法调用栈中或者特定实现中的某个内存区域中。

2. JVM 工做机制

前面简单分析了 JVM 的基本结构,下面简单分析一下 JVM 是如何执行字节码命令的,就是介绍执行引擎是如何进行工做的。

2.1 机器如何执行代码

在分析 JVM 的执行引擎如何工做以前,咱们不妨先看看在普通的实体机上程序是如何执行的。经过下图来带你们理解一下。

计算机只接受机器指令,高级语言必需要通过编译器编译成机器指令才能被计算机执行,因此从高级语言到机器语言之间必需要有个翻译过程。咱们知道机器语言是和硬件平台密切相关,编译器经过编译解决了高级语言与硬件的耦合。那不一样硬件平台就会所需的编译器也是不一样的,如今的硬件平台已经被更上一层的软件平台代替了,这个软件平台就是操做系统。因此就有了上图中 C 语言的编译器在不一样的操做系统是不一样的。固然也有不少不一样的厂家的编译器和操做系统关系不大,只是实现上有差别。

​ 一般一个程序从编写到执行会经历如下一些阶段:

除了源代码和最后的可执行程序,中间的全部环节都是由现代意义上的编译器统一完成的。

如在 Linux 平台上咱们一般安装一个软件须要通过 confrgure、make、make install、make clean 这 4 个步骤来完成。

configure 为这个程序在当前的操做系统环境下选择合适的编译器来编译这个程序代码,也就是为这个程序代码选择合适的编译器和一些环境参数;

make 对程序代码进行编译操做,它会将源码编译成可执行的目标文件,

make install 将已经编译好的可执行文件安装到操做系统指定或者默认的安装目录下。

make clean 删除编译临时产生的目录或文件。

咱们说的编译器一般是高级语言翻译成目标机器语言,也就是低级语言。还有一些编译器是高级语言编辑成高级语言,高级语言编译成虚拟机目标语言(Java 编译器),低级语言翻译成高级语言(反编译)。

如何让机器(不论是实体机仍是虚拟机)执行代码呢?

指令集最基本的元素:加、减、乘、求余、求模等。这些运算又能够进一步分解成二进制运算:与、或、异或等。这些运算又经过指令来完成,而指令的核心目的就是肯定须要运算的种类(操做码)和运算须要的数据(操做数),以及从哪里(寄存器或栈)获取操做数、将运算结果存到什么地方(寄存器或是栈)等。这种不一样的操做方式又将指令划分红:一指令地址、二指令地址、三指令地址和零指令地址等 n 地址指令。相应的这些指令集就会有相应的架构实现,如基于寄存器的架构实现或者基于栈的架构实现,这里的基于寄存器或者栈都是指一个指令中的操做数是如何获取的。

2.2 JVM 为什么选择基于栈的架构

JVM 执行字节码指令是基于栈的架构,也就是全部的操做数必须先入栈,而后根据指令中的操做数选择从栈顶弹出若干元素进行计算,将计算的结果再压入到栈中。

JVM 中操做数能够存放在每个栈帧中的一个本地变量集中,每一个方法调用时会给这个方法分配一个本地变量集,这个本地变量集在编译的时候已经肯定,因此操做数入栈能够直接是常量入栈或者从本地变量中取出一个变量压入栈中。

和通常的基于寄存器的操做有所不一样,一个操做须要频繁地入栈和出栈,进行一个加法运算,若是两个操做数都在本地变量中,一个加法操做就要有 5 次栈操做,分别是将两个操做数从本地变量入栈(2 次入栈操做),再将两个操做数出栈用于加法运算(2 次出栈),再将结果压入栈顶(1 次入栈)。若是是基于寄存器的通常只须要将两个操做数存入寄存器进行加法运算后再将结果存入其中一个寄存器便可,不须要这么多的数据移动操做。那为何 JVM 还要基于栈来设计呢?

  1. JVM 要设计成平台无关的

    要与平台无关,就要保证在没有或者有不多的寄存器的机器上也能运行 Java 代码。

  2. JVM 更好地优化代码

    对于 Java 来讲,JVM 可做为链接器(动态)使用,也能够做为优化器使用。这种以栈为中心的体系结构能够将运行时进行的优化工做与执行即时编译或者自适应优化的执行引擎集合起来,从而能够更好地优化执行 Java 字节码指令。

  3. 为了指令的紧凑性

    操做码能够只占一个字节大小,为了尽可能让编译后的 class 文件更加紧凑,提升字节码在网络上的传输效率。

2.3 执行引擎的架构设计

了解了 Java 以栈为架构的缘由后,再详细看一下 JVM 是如何设计 Java 的执行部件的,以下图所示。

每当建立一个新的线程,JVM 会为这个线程建立一个 Java 栈,同时会为这个线程分配一个 PC 寄存器,而且这个 PC 寄存器会指向这个线程的第一行可执行代码。每当调用一个新方法时会在这个栈上建立一个新的栈帧数据结构,这个栈帧会保留这个方法的一些元信息,如这个方法中定义的局部变量、一些用来支持常量池的解析、正常方法返回及异常处理机制等

JVM 在调用某些指令时可能须要使用到常量池中的一些常量,或者是获取常量表明的数据或者这个数据指向的实例化的对象,而这些信息都存储在全部线程共享的方法区和 Java 堆中。

2.4 执行引擎的执行过程

下面以一个具体的例子看一下执行引擎时如何将一段代码在执行部件执行的,以下一段代码:

public class Math {
    public static void main(String[] args) {
        int a=1;
        int b=2;
        int c=(a+b)*10;
    }
}
复制代码

看一下 main 方法的字节码指令:

0: iconst_1 常量1入栈
     1: istore_1 将栈顶元素移入到本地变量1存储
     2: iconst_2 常量2入栈
     3: istore_2 将栈顶元素移入到本地变量2存储
     4: iload_1  本地变量1入栈
     5: iload_2  本地变量2入栈
     6: iadd 弹出栈顶两个元素相加
     7: bipush 10 将10入栈
     9: imul 栈顶两个元素相乘
    10: istore_3 栈顶元素移入到本地变量3存储
    11: return 返回
复制代码

对应到执行引擎各部件以下图所示。

在开始执行方法以前,PC 寄存器存储的指针是第 1 条指令的地址,局部变量区和操做栈都没有数据。从第 1 条到第 4 条指令分别将 a、b 两个本地变量赋值,对应到局部变量区就是 1 和 2 分别存储常量 1 和 2,以下图所示。

前 4 条指令执行完后,PC 寄存器当前指向的是下一条指令地址,也就是第 5 条指令,这时局部变量区已经保存了两个局部变量(也就是 a 和 b 的值),而操做栈里仍然没有值,由于两次常数入栈后又分别出栈了。

第 5 条和第 6 条指令分别是将两个局部变量入栈,而后相加。

1 先入栈 2 后入栈,栈顶元素是 2,第 7 条指令是将栈顶的两个元素弹出后相加,结果再入栈,这时整个部件状态以下图所示。

当前 PC 寄存器执行的地址是9,下一个操做时将当前栈的两个操做数弹出进行相乘并把结果压入栈中。

第 10 条指令是将当前的栈顶元素存入局部变量 3 中。

第 10 条指令执行完后栈中元素出栈,出栈的元素存储在局部变量区 3 中,对应的是变量 c 的值。最后一条指令是 return,这条指令执行完后当前的这个方法对应的这些部件会被 JVM 回收,局部变量区的全部值将所有释放,PC 寄存器会被销毁,在 Java 栈中与这个方法对应的栈帧将消失。

2.5 JVM 方法调用栈

JVM 的方法调用分为两种,一种是 Java 方法调用,另外一种是本地方法调用。本地方法调用因为各个虚拟机的实现不太相同,因此这里主要介绍 Java 的方法调用状况。

以下面一段方法调用的代码:

public class Math {
    public static void main(String[] args) {
        int a=1;
        int b=2;
        int c=math(a,b)/10;
    }

    public static int math(int a, int b){
        return (a+b)*10;
    }
}
复制代码

对应的字节码以下:

public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iload_2
       6: invokestatic  #2 // Method math:(II)I
       9: bipush        10
      11: idiv
      12: istore_3
      13: return

  public static int math(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: iadd
       3: bipush        10
       5: imul
       6: ireturn
复制代码

当 JVM 执行 main 方法时,首先将两个常量 1 和 2 分别存储到局部变量区 1 和 2 中,而后调用静态 main 方法。从 math 的字节码指令能够看出,math 方法的两个参数也存储在对应的方法栈帧中的局部变量区 0 和 1中,先将这两个局部变量分别入栈,而后进行相加操做再和常数 10 相乘,最后将结果返回。下面看一下世纪的执行操做部件中是如何操做的。

上图是 JVM 执行到第 5 条指令时,执行引擎各部件的状态图,PC 寄存器指向的是下一条执行 main 方法的地址。 当执行 invokestatic 指令时 JVM 会为 math 方法建立一个新的栈帧,而且将两个参数存在 math 方法对应的栈帧的前两个局部变量区中,这时 PC 寄存器会清零,而且会指向 math 方法对应栈帧的第一条指令地址,这时的状态如图所示。

执行 invokestatic 指令时,建立了一个新的栈帧,这时栈帧中的局部变量区已经有两个变量了,这两个变量是从 main 方法的栈帧中的操做栈中传过来的。当执行 math 方法时,math 方法对应的栈帧成为当前活动栈帧,PC 寄存器保存的是当前这个栈帧中的下一条指令地址,因此是 0.

math 方法先将 a、b 两个变量相加,再乘 10,最后返回这个结果执行到第 5 条指令的状态。

math 的操做栈中的栈顶元素相乘的结果是 30,最后一条指令是 ireturn,这条指令是将当前栈帧的栈顶元素返回到调用这个方法的栈中,而这个栈帧也将撤销,PC 寄存器的值回复调用栈的下一条指令地址。

当执行 return 指令时 main 方法对应的栈帧也将撤销,若是当前线程对应的 Java 栈中没有栈帧,这个 Java 栈也将被 JVM 撤销,整个 JVM 退出。

3. 总结

本篇文章主要介绍了 JVM 的体系结构,以及 JVM 的执行引擎和 JVM 指令的过程,实际上 JVM 的设计很是复杂,包括 JVM 在执行字节码时如何来自动优化这些字节码,并将它们再编译成本地代码,也就是 JIT 技术,这个技术再咱们执行测试时可能会有影响,若是你的程序没有通过充分的“预热”,那么得出的结果可能会不许确,例如,JVM 再执行程序时会记录某个方法的执行次数,若是执行的次数到一个阀值时 JIT 就会编译这个方法为本地代码。

在文章的结尾,你们考虑一个问题,为何递归会引起栈溢出呢?

本文是从 《深刻分析 Java Web 技术内幕》第 7 章 摘录的,我在这个基础上加了一些图片便于理解。电子书能够关注个人公众号,回复【电子书】,便可获取。

推荐阅读

学习反射看这一篇就足够了

相关文章
相关标签/搜索