众所周知,Java支持平台无关性、安全性和网络移动性。而Java平台由Java虚拟机和Java核心类所构成,它为纯Java程序提供了统一的编程接口,而无论下层操做系统是什么。正是得益于Java虚拟机,它号称的“一次编译,处处运行”才能有所保障。html
Java程序的执行依赖于编译环境和运行环境。源码代码转变成可执行的机器代码,由下面的流程完成:java
Java技术的核心就是Java虚拟机,由于全部的Java程序都在虚拟机上运行。Java程序的运行须要Java虚拟机、Java API和Java Class文件的配合。Java虚拟机实例负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例就诞生了。当程序结束,这个虚拟机实例也就消亡。程序员
Java的跨平台特性,由于它有针对不一样平台的虚拟机。编程
Java虚拟机的主要任务是装载class文件而且执行其中的字节码。由下图能够看出,Java虚拟机包含一个类装载器(class loader),它能够从程序和API中装载class文件,Java API中只有程序执行时须要的类才会被装载,字节码由执行引擎来执行。bootstrap
当Java虚拟机由主机操做系统上的软件实现时,Java程序经过调用本地方法和主机进行交互。Java方法由Java语言编写,编译成字节码,存储在class文件中。本地方法由C/C++/汇编语言编写,编译成和处理器相关的机器代码,存储在动态连接库中,格式是各个平台专有。因此本地方法是联系Java程序和底层主机操做系统的链接方式。数组
因为Java虚拟机并不知道某个class文件是如何被建立的,是否被篡改一无所知,因此它实现了一个class文件检测器,确保class文件中定义的类型能够安全地使用。class文件检验器经过四趟独立的扫描来保证程序的健壮性:安全
Java虚拟机在执行字节码时还进行其它的一些内置的安全机制的操做,他们做为Java编程语言保证Java程序健壮性的特性,同时也是Java虚拟机的特性:网络
Java虚拟机经过某些数据类型来执行计算。数据类型能够分为两种:基本类型和引用类型,以下图:oracle
但boolean有点特别,当编译器把Java源码编译为字节码时,它会用int或byte表示boolean。在Java虚拟机中,false是由0表示,而true则由全部非零整数表示。和Java语言同样,Java虚拟机的基本类型的值域在任何地方都是一致的,无论主机平台是什么,一个long在任何虚拟机中老是一个64位二进制补码的有符号整数。app
对于returnAddress,这个基本类型被用来实现Java程序中的finally子句,Java程序员不能使用这个类型,它的值指向一条虚拟机指令的操做码。
在 Java虚拟机规范中,一个虚拟机实例的行为是分别按照子系统、内存区、数据类型和指令来描述的,这些组成部分一块儿展现了抽象的虚拟机的内部体系结构。
Java class文件包含了关于类或接口的全部信息。class文件的“基本类型”以下:
u1 | 1个字节,无符号类型 |
u2 | 2个字节,无符号类型 |
u4 | 4个字节,无符号类型 |
u8 | 8个字节,无符号类型 |
若是想了解更多,Oracle的JVM SE7给出了官方规范:The Java® Virtual Machine Specification
class文件包含的内容:
ClassFile { u4 magic; //魔数:0xCAFEBABE,用来判断是不是Java class文件 u2 minor_version; //次版本号 u2 major_version; //主版本号 u2 constant_pool_count; //常量池大小 cp_info constant_pool[constant_pool_count-1]; //常量池 u2 access_flags; //类和接口层次的访问标志(经过|运算获得) u2 this_class; //类索引(指向常量池中的类常量) u2 super_class; //父类索引(指向常量池中的类常量) u2 interfaces_count; //接口索引计数器 u2 interfaces[interfaces_count]; //接口索引集合 u2 fields_count; //字段数量计数器 field_info fields[fields_count]; //字段表集合 u2 methods_count; //方法数量计数器 method_info methods[methods_count]; //方法表集合 u2 attributes_count; //属性个数 attribute_info attributes[attributes_count]; //属性表 }
类装载器子系统负责查找并装载类型信息。其实Java虚拟机有两种类装载器:系统装载器和用户自定义装载器。前者是Java虚拟机实现的一部分,后者则是Java程序的一部分。
除了系统提供的类装载器之外,开发人员能够经过继承 java.lang.ClassLoader类的方式实现本身的类装载器,以知足一些特殊的需求。
类装载器子系统涉及Java虚拟机的其它几个组成部分以及来自java.lang库的类。ClassLoader定义的方法为程序提供了访问类装载器机制的接口。此外,对于每个被装载的类型,Java虚拟机都会为它建立一个java.lang.Class类的实例来表明该类型。和其它对象同样,用户自定义的类装载器以及Class类的实例放在内存中的堆区,而装载的类型信息则位于方法区。
类装载器子系统除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及解析符号引用。这些动做还须要按照如下顺序进行:
在Java虚拟机中,关于被装载的类型信息存储在一个方法区的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,而后读入这个class文件并将它传输到虚拟机中,接着虚拟机提取其中的类型信息,并将这些信息存储到方法区。方法区也能够被垃圾回收器收集,由于虚拟机容许经过用户定义的类装载器来动态扩展Java程序。
方法区中存放了如下信息:
Java程序在运行时建立的全部类实例或数组(数组在Java虚拟机中是一个真正的对象)都放在同一个堆中。因为Java虚拟机实例只有一个堆空间,因此全部线程都将共享这个堆。须要注意的是,Java虚拟机有一条在堆中分配对象的指令,却没有释放内存的指令,由于虚拟机把这个任务交给垃圾收集器处理。Java虚拟机规范并无强制规定垃圾收集器,它只要求虚拟机实现必须“以某种方式”管理本身的堆空间。好比某个实现可能只有固定大小的堆空间,当空间填满,它就简单抛出OutOfMemory异常,根本不考虑回收垃圾对象的问题,但倒是符合规范的。
Java虚拟机规范并无规定Java对象在堆中如何表示,这给虚拟机的实现者决定怎么设计。一个可能的堆设计以下:
一个句柄池,一个对象池。一个对象的引用就是一个指向句柄池的本地指针。这种设计的好处有利于堆碎片的整理,当移动对象池中的对象时,句柄部分只需更改一下指针指向对象的新地址便可。缺点是每次访问对象的实例变量都要通过两次指针传递。
每当启动给一个线程时,Java虚拟机会为它分配一个Java栈。Java栈由许多栈帧组成,一个栈帧包含一个Java方法调用的状态。当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中,当该方法返回时,这个栈帧就从Java栈中弹出。Java栈存储线程中Java方法调用的状态--包括局部变量、参数、返回值以及运算的中间结果等。Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。这样设计的缘由是为了保持Java虚拟机的指令集尽可能紧凑,同时也便于Java虚拟机在只有不多通用寄存器的平台上实现。另外,基于栈的体系结构,也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。
栈帧由局部变量区、操做数栈和帧数据区组成。当虚拟机调用一个Java方法时,它从对应类的类型信息中获得此方法的局部变量区和操做数栈的大小,并根据此分配栈帧内存,而后压入Java栈中。
局部变量区被组织为以字长为单位、从0开始计数的数组。字节码指令经过从0开始的索引使用其中的数据。类型为int, float, reference和returnAddress的值在数组中占据一项,而类型为byte, short和char的值在存入数组前都被转换为int值,也占据一项。但类型为long和double的值在数组中却占据连续的两项。
和局部变量区同样,操做数栈也是被组织成一个以字长为单位的数组。它经过标准的栈操做访问--压栈和出栈。因为程序计数器没法被程序指令直接访问,Java虚拟机的指令是从操做数栈中取得操做数,因此它的运行方式是基于栈而不是基于寄存器。虚拟机把操做数栈做为它的工做区,由于大多数指令都要从这里弹出数据,执行运算,而后把结果压回操做数栈。
除了局部变量区和操做数栈,Java栈帧还须要帧数据区来支持常量池解析、正常方法返回以及异常派发机制。每当虚拟机要执行某个须要用到常量池数据的指令时,它会经过帧数据区中指向常量池的指针来访问它。除了常量池的解析外,帧数据区还要帮助虚拟机处理Java方法的正常结束或异常停止。若是经过return正常结束,虚拟机必须恢复发起调用的方法的栈帧,包括设置程序计数器指向发起调用方法的下一个指令;若是方法有返回值,虚拟机须要将它压入到发起调用的方法的操做数栈。为了处理Java方法执行期间的异常退出状况,帧数据区还保存一个对此方法异常表的引用。
对于一个运行中的Java程序而言,每个线程都有它的程序计数器。程序计数器也叫PC寄存器。程序计数器既能持有一个本地指针,也能持有一个returnAddress。当线程执行某个Java方法时,程序计数器的值老是下一条被执行指令的地址。这里的地址能够是一个本地指针,也能够是方法字节码中相对该方法起始指令的偏移量。若是该线程正在执行一个本地方法,那么此时程序计数器的值是“undefined”。
任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会建立一个新的栈帧并压入Java栈。当它调用的是本地方法时,虚拟机会保持Java栈不变,再也不在线程的Java栈中压入新的栈,虚拟机只是简单地动态链接并直接调用指定的本地方法。
其中方法区和堆由该虚拟机实例中全部线程共享。当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,而后把这些类型信息放到方法区。当程序运行时,虚拟机会把全部该程序在运行时建立的对象放到堆中。
像其它运行时内存区同样,本地方法栈占用的内存区能够根据须要动态扩展或收缩。
在Java虚拟机规范中,执行引擎的行为使用指令集定义。实现执行引擎的设计者将决定如何执行字节码,实现能够采起解释、即时编译或直接使用芯片上的指令执行,还能够是它们的混合。
执行引擎能够理解成一个抽象的规范、一个具体的实现或一个正在运行的实例。抽象规范使用指令集规定了执行引擎的行为。具体实现可能使用多种不一样的技术--包括软件方面、硬件方面或树种技术的结合。做为运行时实例的执行引擎就是一个线程。
运行中Java程序的每个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,它要么在执行字节码,要么执行本地方法。
方法的字节码流由Java虚拟机的指令序列构成。每一条指令包含一个单字节的操做码,后面跟随0个或多个操做数。操做码表示须要执行的操做;操做数向Java虚拟机提供执行操做码须要的额外信息。当虚拟机执行一条指令时,可能使用当前常量池中的项、当前帧的局部变量中的值或者位于当前帧操做数栈顶端的值。
抽象的执行引擎每次执行一条字节码指令。Java虚拟机中运行的程序的每一个线程(执行引擎实例)都执行这个操做。执行引擎取得操做码,若是操做码有操做数,就取得它的操做数。它执行操做码和跟随的操做数规定的动做,而后再取得下一个操做码。这个执行字节码的过程在线程完成前将一直持续,经过从它的初始方法返回,或者没有捕获抛出的异常均可以标志着线程的完成。
Java本地接口,也叫JNI(Java Native Interface),是为可移植性准备的。本地方法接口容许本地方法完成如下工做:
参考:
《深刻Java虚拟机》