深刻理解JVM(重要)

1   Java技术与Java虚拟机 java

提及Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。它们的关系以下图所示:程序员

图1   Java四个方面的关系算法

运行期环境表明着Java平台,开发人员编写Java代码(.java文件),而后将之编译成字节码(.class文件)。最后字节码被装入内存, 一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。从上图也能够看出Java平台由Java虚拟机和 Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序能够运行在这个平台上。这个平台的结构以下图所示:编程

在Java平台的结构中, 能够看出,Java虚拟机(JVM) 处在核心的位置,是程序与底层操做系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操做系统, 其中依赖于平台的部分称为适配器;JVM 经过移植接口在具体的平台和操做系统上实现;在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 能够在任何Java平台上运行而无需考虑底层平台, 就是由于有Java虚拟机(JVM)实现了程序与操做系统的分离,从而实现了Java 的平台无关性。小程序

那么到底什么是Java虚拟机(JVM)呢?一般咱们谈论JVM时,咱们的意思多是:数组

  1. 对JVM规范的的比较抽象的说明;
  2. 对JVM的具体实现;
  3. 在程序运行期间所生成的一个JVM实例。

对JVM规范的的抽象说明是一些概念的集合,它们已经在书《The Java Virtual Machine Specification》(《Java虚拟机规范》)中被详细地描述了;对JVM的具体实现要么是软件,要么是软件和硬件的组合,它已经被许多生产厂 商所实现,并存在于多种平台之上;运行Java程序的任务由JVM的运行期实例单个承担。在本文中咱们所讨论的Java虚拟机(JVM)主要针对第三种情 况而言。它能够被当作一个想象中的机器,在实际的计算机上经过软件模拟来实现,有本身想象中的硬件,如处理器、堆栈、寄存器等,还有本身相应的指令系统。安全

JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,所以当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。下面咱们从JVM的体系结构和它的运行过程这两个方面来对它进行比较深刻的研究。app

2   Java虚拟机的体系结构 编程语言

刚才已经提到,JVM能够由不一样的厂商来实现。因为厂商的不一样必然致使JVM在实现上的一些不一样,然而JVM仍是能够实现跨平台的特性,这就要归功于设计JVM时的体系结构了。ide

咱们知道,一个JVM实例的行为不光是它本身的事,还涉及到它的子系统、存储区域、数据类型和指令这些部分,它们描述了JVM的一个抽象的内部体系 结构,其目的不光规定实现JVM时它内部的体系结构,更重要的是提供了一种方式,用于严格定义实现时的外部行为。每一个JVM都有两种机制,一个是装载具备 合适名称的类(类或是接口),叫作类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫作运行引擎。每一个JVM又包括方法区、堆、 Java栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一块儿组成的体系结构图为:

图3   JVM的体系结构

JVM的每一个实例都有一个它本身的方法域和一个堆,运行于JVM内的全部的线程都共享这些区域;当虚拟机装载类文件的时候,它解析其中的二进制数据 所包含的类信息,并把它们放到方法域中;当程序运行的时候,JVM把程序初始化的全部对象置于堆上;而每一个线程建立的时候,都会拥有本身的程序计数器和 Java栈,其中程序计数器中的值指向下一条即将被执行的指令,线程的Java栈则存储为该线程调用Java方法的状态;本地方法调用的状态被存储在本地 方法栈,该方法栈依赖于具体的实现。

下面分别对这几个部分进行说明。

执行引擎处于JVM的核心位置,在Java虚拟机规范中,它的行为是由指令集所决定的。尽管对于每条指令,规范很详细地说明了当JVM执行字节码遇 到指令时,它的实现应该作什么,但对于怎么作却言之甚少。Java虚拟机支持大约248个字节码。每一个字节码执行一种基本的CPU运算,例如,把一个整数 加到寄存器,子程序转移等。Java指令集至关于Java程序的汇编语言。

Java指令集中的指令包含一个单字节的操做符,用于指定要执行的操做,还有0个或多个操做数,提供操做所需的参数或数据。许多指令没有操做数,仅由一个单字节的操做符构成。

虚拟机的内层循环的执行过程以下:  do{  取一个操做符字节;  根据操做符的值执行一个动做;  }while(程序未结束)

因为指令系统的简单性,使得虚拟机执行的过程十分简单,从而有利于提升执行的效率。指令中操做数的数量和大小是由操做符决定的。若是操做数比一个字节大,那么它存储的顺序是高位字节优先。例如,一个16位的参数存放时占用两个字节,其值为:

第一个字节*256+第二个字节字节码。

指令流通常只是字节对齐的。指令tableswitch和lookup是例外,在这两条指令内部要求强制的4字节边界对齐。

对于本地方法接口,实现JVM并不要求必定要有它的支持,甚至能够彻底没有。Sun公司实现Java本地接口(JNI)是出于可移植性的考虑,固然 咱们也能够设计出其它的本地接口来代替Sun公司的JNI。可是这些设计与实现是比较复杂的事情,须要确保垃圾回收器不会将那些正在被本地方法调用的对象 释放掉。

Java的堆是一个运行时数据区,类的实例(对象)从中分配空间,它的管理是由垃圾回收来负责的:不给程序员显式释放对象的能力。Java不规定具体使用的垃圾回收算法,能够根据系统的需求使用各类各样的算法。

Java方法区与传统语言中的编译后代码或是Unix进程中的正文段相似。它保存方法代码(编译后的java代码)和符号表。在当前的Java实现 中,方法代码不包括在垃圾回收堆中,但计划在未来的版本中实现。每一个类文件包含了一个Java类或一个Java界面的编译后的代码。能够说类文件是 Java语言的执行代码文件。为了保证类文件的平台无关性,Java虚拟机规范中对类文件的格式也做了详细的说明。其具体细节请参考Sun公司的Java 虚拟机规范。

Java虚拟机的寄存器用于保存机器的运行状态,与微处理器中的某些专用寄存器相似。Java虚拟机的寄存器有四种:

  1. pc: Java程序计数器;
  2. optop: 指向操做数栈顶端的指针;
  3. frame: 指向当前执行方法的执行环境的指针;。
  4. vars: 指向当前执行方法的局部变量区第一个变量的指针。

在上述体系结构图中,咱们所说的是第一种,即程序计数器,每一个线程一旦被建立就拥有了本身的程序计数器。当线程执行Java方法的时候,它包含该线程正在被执行的指令的地址。可是若线程执行的是一个本地的方法,那么程序计数器的值就不会被定义。

Java虚拟机的栈有三个区域:局部变量区、运行环境区、操做数区。

局部变量区

每一个Java方法使用一个固定大小的局部变量集。它们按照与vars寄存器的字偏移量来寻址。局部变量都是32位的。长整数和双精度浮点数占据了两 个局部变量的空间,却按照第一个局部变量的索引来寻址。(例如,一个具备索引n的局部变量,若是是一个双精度浮点数,那么它实际占据了索引n和n+1所代 表的存储空间)虚拟机规范并不要求在局部变量中的64位的值是64位对齐的。虚拟机提供了把局部变量中的值装载到操做数栈的指令,也提供了把操做数栈中的 值写入局部变量的指令。

运行环境区

在运行环境中包含的信息用于动态连接,正常的方法返回以及异常捕捉。

动态连接

运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态连接。方法的class文件代码在引用要调用的方法和要访问的变 量时使用符号。动态连接把符号形式的方法调用翻译成实际方法调用,装载必要的类以解释尚未定义的符号,并把变量访问翻译成与这些变量运行时的存储结构相 应的偏移地址。动态连接方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码。

正常的方法返回

若是当前方法正常地结束了,在执行了一条具备正确类型的返回指令时,调用的方法会获得一个返回值。执行环境在正常返回的状况下用于恢复调用者的寄存器,并把调用者的程序计数器增长一个恰当的数值,以跳过已执行过的方法调用指令,而后在调用者的执行环境中继续执行下去。

异常捕捉

异常状况在Java中被称做Error(错误)或Exception(异常),是Throwable类的子类,在程序中的缘由是:①动态连接错,如没法找到所需的class文件。②运行时错,如对一个空指针的引用。程序使用了throw语句。

当异常发生时,Java虚拟机采起以下措施:

  • 检查与当前方法相联系的catch子句表。每一个catch子句包含其有效指令范围,可以处理的异常类型,以及处理异常的代码块地址。
  • 与异常相匹配的catch子句应该符合下面的条件:形成异常的指令在其指令范围以内,发生的异常类型是其能处理的异常类型的子类型。若是找到了匹 配的catch子句,那么系统转移到指定的异常处理块处执行;若是没有找到异常处理块,重复寻找匹配的catch子句的过程,直到当前方法的全部嵌套的 catch子句都被检查过。
  • 因为虚拟机从第一个匹配的catch子句处继续执行,因此catch子句表中的顺序是很重要的。由于Java代码是结构化的,所以总能够把某个方 法的全部的异常处理器都按序排列到一个表中,对任意可能的程序计数器的值,均可以用线性的顺序找到合适的异常处理块,以处理在该程序计数器值下发生的异常 状况。
  • 若是找不到匹配的catch子句,那么当前方法获得一个"未截获异常"的结果并返回到当前方法的调用者,好像异常刚刚在其调用者中发生同样。若是 在调用者中仍然没有找到相应的异常处理块,那么这种错误将被传播下去。若是错误被传播到最顶层,那么系统将调用一个缺省的异常处理块。

操做数栈区

机器指令只从操做数栈中取操做数,对它们进行操做,并把结果返回到栈中。选择栈结构的缘由是:在只有少许寄存器或非通用寄存器的机器(如 Intel486)上,也可以高效地模拟虚拟机的行为。操做数栈是32位的。它用于给方法传递参数,并从方法接收结果,也用于支持操做的参数,并保存操做 的结果。例如,iadd指令将两个整数相加。相加的两个整数应该是操做数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整数将从堆栈弹出、相 加,并把结果压回到操做数栈中。

每一个原始数据类型都有专门的指令对它们进行必须的操做。每一个操做数在栈中须要一个存储位置,除了long和double型,它们须要两个位置。操做 数只能被适用于其类型的操做符所操做。例如,压入两个int类型的数,若是把它们看成是一个long类型的数则是非法的。在Sun的虚拟机实现中,这个限 制由字节码验证器强制实行。可是,有少数操做(操做符dupe和swap),用于对运行时数据区进行操做时是不考虑类型的。

本地方法栈,当一个线程调用本地方法时,它就再也不受到虚拟机关于结构和安全限制方面的约束,它既能够访问虚拟机的运行期数据区,也可使用本地处理 器以及任何类型的栈。例如,本地栈是一个C语言的栈,那么当C程序调用C函数时,函数的参数以某种顺序被压入栈,结果则返回给调用函数。在实现Java虚 拟机时,本地方法接口使用的是C语言的模型栈,那么它的本地方法栈的调度与使用则彻底与C语言的栈相同。

3   Java虚拟机的运行过程

上面对虚拟机的各个部分进行了比较详细的说明,下面经过一个具体的例子来分析它的运行过程。

虚拟机经过调用某个指定类的方法main启动,传递给main一个字符串数组参数,使指定的类被装载,同时连接该类所使用的其它的类型,而且初始化它们。例如对于程序:

class HelloApp  {  public static void main(String[] args)   {   System.out.println("Hello World!");    for (int i = 0; i < args.length; i++ )   {    System.out.println(args[i]);   }  } }

编译后在命令行模式下键入: java HelloApp run virtual machine

将经过调用HelloApp的方法main来启动java虚拟机,传递给main一个包含三个字符串"run"、"virtual"、"machine"的数组。如今咱们略述虚拟机在执行HelloApp时可能采起的步骤。

开始试图执行类HelloApp的main方法,发现该类并无被装载,也就是说虚拟机当前不包含该类的二进制表明,因而虚拟机使用 ClassLoader试图寻找这样的二进制表明。若是这个进程失败,则抛出一个异常。类被装载后同时在main方法被调用以前,必须对类 HelloApp与其它类型进行连接而后初始化。连接包含三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则建立类或接口的静态域以 及把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的静态初始化函数和静态域的初 始化构造方法的执行。一个类在初始化以前它的父类必须被初始化。整个过程以下:

图4:虚拟机的运行过程

4   结束语

本文经过对JVM的体系结构的深刻研究以及一个Java程序执行时虚拟机的运行过程的详细分析,意在剖析清楚Java虚拟机的机理。

相关文章
相关标签/搜索