首先来看一下Java程序的执行过程java
首先是.java源文件被编译成为.class类文件(又叫字节码),JRE的类加载器从硬盘中读取class文件,载入到系统分配给JVM的内存区域--运行数据区(Runtime Data Areas). 而后执行引擎解释或者编译类文件,转化成特定CPU的机器码,CPU执行机器码,至此完成整个过程.上面图了解明白后再看下面这个图应该理解更深刻一些。程序员
接下来是类加载器算法
下面是类加载器的层级结构,也就是父子关系。Bootstrap是全部类加载器的父亲。数组
--Bootstrap class loader:安全
当运行java虚拟机时,这个类加载器被建立,它加载一些基本的java API,包括Object这个类。须要注意的是,这个类加载器不是用java语言写的,而是用C/C++写的。函数
--Extension class loader:性能
这个加载器加载除了基本API以外的一些拓展类,包括一些与安全性能相关的类。(目前了解得不是很深,只能笼统说,待往后再详细说明)spa
--System Class Loader:.net
它加载应用程序中的类,也就是在你的classpath中配置的类。命令行
--User-Defined Class Loader:
这是开发人员经过拓展ClassLoader类定义的自定义加载器,加载程序员定义的一些类。
》》委派模式(Delegation Mode)
仔细看上面的层次结构,当JVM加载一个类的时候,下层的加载器会将任务委托给上一层类加载器,上一层加载检查它的命名空间中是否已经加载这个 类,若是已经加载,直接使用这个类。若是没有加载,继续往上委托直到顶部。检查完了以后,按照相反的顺序进行加载,若是Bootstrap加载器找不到这 个类,则往下委托,直到找到类文件。对于某个特定的类加载器来讲,一个Java类只能被载入一次,也就是说在Java虚拟机中,类的完整标识是 (classLoader,package,className)。一个类能够被不一样的类加载器加载。
举个具体的例子来讲明,如今加入我有一个本身定义的类MyClass须要加载,若是不指定的话,通常交App(System)加载。接到任务 后,System检查本身的库里是否已经有这个类,发现没有以后委托给Extension,Extension进行一样的检查,发现仍是没有继续往上委 托,最顶层的Boots发现本身库里也没有,因而根据它的路径(Java 核心类库,如java.lang)尝试去加载,没找到这个MaClass类,因而只好(人家看好你,交给你完成,你无能为力,只好交给别人啦)往下委托给 Extension,Extension到本身的路径(JAVA_HOME/jre/lib/ext)是找,仍是没找到,继续往下,此时System加载 器到classpath路径寻找,找到了,因而加载到Java虚拟机。
如今假设咱们将这个类放到JAVA_HOME/jre/lib/ext这个路径中去(至关于交给Extension加载器加载),按照一样的规则, 最后由Extension加载器加载MyClass类,看到了吧,统一各种被两次加载到JVM,可是每次都是由不一样的ClassLoader完成。
》》可见性限制
下层的加载器可以看到上层加载器中的类,反之则不行,也就是是说委托只能从下到上。
》》不容许卸载类
类加载器能够加载一个类,可是它不能卸载一个类。可是类加载器能够被删除或者被建立。
一个JVM实例的行为不光是它本身的事,还涉及到它的子系统、存储区域、数据类型和指令这些部分,它们描述了JVM的一个抽象的内部体系结构,其目的不光规定实现JVM时它内部的体系结构,更重要的是提供了一种方式,用于严格定义实现时的外部行为。每一个JVM都有两种机制,一个是装载具备合适名称的类(类或是接口),叫作类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫作运行引擎。每一个JVM又包括方法区、堆、Java栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一块儿组成的体系结构图为:
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虚拟机的机理。