JVM体系结构与工做方式概览

Java之因此号称“一次编译,处处运行”,主要缘由是JVM屏蔽了各个计算机平台相关的软件(大多指系统)或者硬件之间的差别,使得与平台相关的耦合统一由JVM提供者来实现。在本文,笔者将与你们概览JVM的体系结构与工做方式。java

JVM体系结构详解

JVM和实体机器的体系结构有点类似,主要由如下几个部分组成:linux

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

JVM-System.jpg

下面简单介绍一下android

执行引擎

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

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

Java内存管理

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

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

考虑到篇幅大小,故另写一篇文章:浅析JVM以内存管理性能

JVM工做机制

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

咱们知道,计算机只接受机器指令,其余高级语言必须先通过编译器编译成机器指令才能被计算机正确执行。然而机器语言通常和硬件平台密切相关(指令集、CPU架构的因素等),但高级语言会屏蔽全部底层硬件平台甚至软件平台。之因此能够屏蔽是由于中间有个编译环节,与硬件耦合的麻烦就交给了编译器。因此,想说的是:编译器和操做系统的关系很是密切。好比C语言在win下编译器为Microsoft C,而Linux下一般是gcc。spa

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

  1. 源代码(source code)
  2. 预处理器(preprocessor)
  3. 编译器(compiler)
  4. 汇编程序(assembler)
  5. 目标代码(object code)
  6. 连接器(linker)
  7. 可执行程序(executables)

除了一、7两步,其余都是由现代意义上的编译器统一完成的。最多见的栗子是在Linux平台下咱们一般安装一个软件须要通过configure、make、make install、make clean这4个步骤来完成。

  • configure为这个程序在当前的操做系统环境下选择适合的编译器来编译这个程序代码,也就是为这个程序代码选择合适的编译器和一些环境参数;
  • make 能够猜到:对程序代码进行编译操做,将源码编译可执行的目标文件
  • make install 将已经编译好的可执行文件安装到操做系统指定或者默认的安装目录下
  • make clean 用删除编译时临时产生的目标文件

值得注意的是,咱们一般所说的是编译器都是将某种高级语言直接编译成可执行的目标机器语言(实际上在某种操做系统中是ixuyao动态链接的二进制文件:在Windows下是dynamic link library,Dll;在linux下是Shared library,SO库)。可是实际上还有一些编译是将一种高级语言编译成另外一种高级语言,或者将低级语言编译成高级语言(反编译),或者将高级语言编译成虚拟机目标语言,如Java编译器等。

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

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

学过数据结构的小伙伴都知道,对栈进行操做是要先将全部的操做数压入栈,而后根据指令中操做码选择必定的元素弹出计算后再压入栈。相对于寄存器操做(将两个操做数存入寄存器后进行加法运算后再将加过存入其中一个寄存器便可)是比较麻烦的。那么,JVM为何还要基于栈来设计呢

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

    • 有些平台上的寄存器不多或者根本没有,并且以处理器架构的角度来讲,设计一套通用的寄存器指令是很困难的。好比在android上,google的Dalvik VM就是基于ARM平台设计的寄存器架构,这样性能上的确更优了,可是牺牲了跨平台的移植性。
  2. 为了指令的紧凑型

执行引擎的架构设计

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

JavaExecuteComponent.jpg

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

 执行引擎的执行过程

下面以一个简单的程序来讲明执行引擎的执行过程。

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

其中对应的字节码指令以下:

偏移量 指令 说明
0 iconst_1 常数1入栈
1 istore_1 将栈顶元素移入本地变量1存储
2 iconst_1 常数2入栈
3 istore_2 将栈顶元素移入本地变量1存储
4 iload_1 本地变量1入栈
5 iload_2 本地变量2入栈
6 iadd 弹出栈顶两个元素相加
7 bipush 10 将10入栈
9 imul 栈顶两个元素相乘
10 istore_3 栈顶元素移入本地变量3存储
11 return 返回

对应到执行引擎的各执行部件如图

执行引擎过程分析1-1.jpg

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

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

执行引擎过程分析1-2.jpg

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

执行引擎过程分析1-3.jpg

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

执行引擎过程分析1-4.jpg

能够看出,变量a和变量b想加的结果3存在当前栈的栈顶中,接下来是第8条指令将10入栈,如图

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

执行引擎过程分析1-5.jpg

第10条指令是将当前的栈顶元素存入局部变量3中,这是状态如图

执行引擎过程分析1-6.jpg

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

 JVM方法调用栈

JVM的方法调用分别为两种:

  1. Java方法调用
  2. 本地方法调用

因为本地方法调用各个虚拟机的实现不太相同,因此这里主要介绍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)
    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中,而后调用静态math方法。从math的字节码指令能够看出,math方法的两个参数也存储在其对应的方法栈帧中的局部变量区0和1中,先将这两个局部变量分别入栈,而后进行相加操做再和常数10相乘。

那么来看一下实际的操做,如图

JVM方法调用栈分析2-0.jpg

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

JVM方法调用栈程分析2-1.jpg

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

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

JVM方法调用栈过程分析2-2.jpg

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

JVM方法调用栈过程分析2-3.jpg

main方法将math方法返回的结果再除以10存放在变量区3中,这时的状态如图所示

JVM方法调用栈过程分析2-4.jpg

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

相关文章
相关标签/搜索