JVM(Java虚拟机)是一个抽象的计算模型。就如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域。目的是为构建在其上运行的应用程序提供一个运行环境。JVM可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构。本文主要对JVM进行概述,并介绍Java程序是如何在上面执行的。
虚拟机
从本质上讲,虚拟机是个被构建来提供特定或通用目的服务环境的非实体计算机。这听起来像是一个仿真器,用来仿真机器未配置或不能按要求执行任务的硬件组件。因此,我们要做的就是创建一个软件,以软件的形式模拟硬件提供的服务,使之看起来这个特定的硬件在系统中是实际存在的。虚拟机在一定程度上使用CPU虚拟化,为实际的硬件问题提供一个接口。所以可以说它实际上有两种功能:提供一个虚拟的环境,或者将某些不存在的事物进行抽象化。但是当我们深入了解之后会发现,这两种功能有着很明显的不同。我们现在暂且不看它们之间的不同点,它们的共同点在于都“假装”成它们不是的东西。正如Popek和Goldberg在论文“Formal Requirements for Virtualizable Third Generation Architectures”里说的,它是“一个真实机器有效、独立的复制品。”
基于不同的需求和用途,虚拟机有很多类型。一种叫完全虚拟化(full virtualization),这种虚拟机表现得像一台真正的机器。其他类型的虚拟机会更精细,更专业,比如进程虚拟化(process virtualization)。而对JVM进行分类是很困难的,因为它对CPU进行了虚拟化,有自己的运行时环境、与底层平台协调工作的内存管理器、垃圾收集器,当然还有大量作为中间字节码输入的类库,最后但同样重要的是,它能够模拟机器的寄存器、堆栈等等。简单地说,它是被Java编译器编译为java的本质——字节码的游乐场。字节码实际上是JVM用来将代码重新翻译为本地机器指令所使用的机器代码。
类文件格式
有趣的是,其实JVM并不关心Java语言或其他编程语言的语义和语法结构。当JVM执行一段程序的时候,它主要关注的是一种称为“类文件”的特定文件格式。*.class类文件格式和Java代码定义的面向对象的类结构毫无关系。编译器将*.java文件编译成*.class文件,然后JVM对*.class文件进行解译,它不关心这个类文件是由哪种编译器生成的,只要符合类文件的文件格式即可。Java编译器将一段程序编译为等价的类文件。这些类文件实际上包含了半编译的代码——字节码。之所以称之为半编译,是因为字节码并不像C/C++编译器编译的二进制文件一样会被直接执行。字节码要先被输入到JVM中,然后再转换为底层平台可以执行的最终指令。所以字节码包含了JVM的指令、符号表和其他的辅助信息。不管何种语言,能根据JVM的语法和结构约束编译生成字节码的编译器,都是一个可以在JVM上执行的候选者。
JVM的定位
JVM将自身定位于字节码和底层平台之间。底层平台是指操作系统(OS)和硬件。操作系统和硬件体系结构在不同的机器上可能不同,但是同一段Java程序可以不用做任何的代码修改就能在不同的机器上运行。这是在虚拟环境中执行的程序语言的独特之处。例如,由其他程序语言编译器编译的目标代码如C++和Java相比的不同点在于,C++程序需要被特定平台的编译器重新编译,从而使它能在不同的体系结构上面运行。而Java代码并不需要做任何改变,因为由Java编译器编译的字节码是在外围的JVM上执行。因此,JVM负责重新解译由Java编译器生成的字节码,并和底层平台协调工作。也就是说,尽管Java编译器生成的结果是平台独立的,但JVM与特定平台相关的。除非两台机器有相同的体系结构,在某个体系结构上安装和使用的JVM可能换一台机器就不能正常工作了。
图1:JVM结构
相对于JVM, JRE和JDK又是什么?
想要运行Java程序,我们需要JVM因为它提供了字节码的运行环境。Oracle提供了两种不同的产品:JDK(Java开发工具)和JRE(Java运行环境)。JRE是我们安装运行Java程序的最基本软件。它和Java类库以及运行Java程序所需要的其他组件一起够成了JVM的一个实现。所以,如果我们想运行一个类文件或一段字节码,仅需要JRE就够了。而JDK(Java开发工具)是JRE的超集。它包含了JRE提供的所有东西,包括创建类文件的工具如Java编译器、调试器和其他许多开发Java程序相关的工具。所以,当我们要创建类文件(编译Java源码)时,我们就需要JDK。下面是一张Java API文档的截图。注意组成JDK,JRE和Java SE API核心类库的组件;通过这张截图你可以了解JRE和JDK里面都有哪些内容。
图2: 来自Java API文档
Java提供了Java虚拟机规范来让我们对JVM的工作原理有一个完整的认识。你可以从这里得到概念性知识,并开发一个自己的JVM;但这并不是一个简单的工作。现在市场上已经有很多JVM了,其中有些是免费的,还有一些需要购买商业许可证才能使用。
在JVM上执行Java程序
每一个在JRE上运行的Java程序都会创建一个JVM实例。编译后的Java类文件和其他被依赖的类文件会被加载到运行环境中。这一步由类加载器协助完成。
图3:类加载模块和其功能
类加载器通过三步完成类加载
首先,类加载器会以字节码的形式加载程序类文件和与JDK绑定的标准Java类文件。标准类文件构成了Java API核心类库。引导程序通过定位通常位于jre/lib目录下的核心API类库启动。
然后,扩展机制定位扩展类库,例如一些为开发或执行代码而被添加到Java里新的(可选)包。扩展类通常位于 jre/lib/ext目录下。有时,扩展类会被放到系统属性java.ext.dirs 定义的其他目录下面。程序包使用JAR或ZIP的扩展名。
最后,如果要加载的类没有在Java的标准类库或扩展类库中被找到,加载器会搜索CLASSPATH环境变量下定义的文件路径,CLASSPATH里面包含了诸多存储类文件的地址。系统属性java.class.path对CLASSPATH环境变量做了映射。
像JAR或ZIP这样的归档文件都是包含了一些其他文件目录的独立文件,通常是压缩文件格式。例如,程序中使用的标准类库包含在归档文件 rt.jar中,该文件会和JDK被一同安装。
一旦文件被定位并加载之后,类加载器会执行不同的功能,例如根据JVM的约束进行校验、内存分配,或者在调用构造器设置定义的变量元素之前使用默认值初始化类变量。
当加载程序结束之后,字节码指令被传递给执行引擎。然后JVM借助于绑定到指定平台的特定JVM实现的本地代码和底层操作系统进行交互。请注意,不同平台的实现可能有略微不同。
数据存储区的堆空间用于存储动态或临时分配的内存空间。类和数组是在这块区域里创建的。当创建对象大小超出堆内存空间时,垃圾收集器会回收内存。
Java栈,又叫栈帧,用于存储局部变量和不同阶段方法调用的临时结果。每一次方法调用都会创建一个栈帧。
方法区基本上是JVM线程间的共享存储区。
寄存器是一个模拟的底层机器寄存器,主要用于执行字节码指令。PC寄存器或程序计数器是用于保存当前指令执行地址的主要寄存器。
JVM功能概述
JVM的功能可以归纳为:
- 加载:通过类加载器加载类文件的过程。
- 链接:链接类文件,提交给JVM在运行时执行。
- 初始化:分配内存和调用类初始化方法设置变量值。
总结
使用虚拟机执行程序的最大好处是它是平台独立的。和C/C++这种高效的语言相比,这种类型编程语言的生产力可以弥补其性能上的弱点。本文仅仅是对JVM的一点浅见,但也许已经足以帮助理解JVM是如何实际工作的。