Java虚拟机的深入研究
作者:刘学?
1 Java技术与Java虚拟?
说vJavaQh们首先想到的是Java~程语言Q然而事实上QJava是一U技术,它由四方面组? Java~程语言、JavacL件格式、Java虚拟机和Java应用E序接口(Java API)。它们的关系如下图所C:
? Java四个斚w的关p?
q行期环境代表着Javaq_Q开发h员编写Java代码(.java文g)Q然后将之编译成字节?.class文g)。最后字节码被装入内存,一旦字节码q入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的{换成机器码执行。从上图也可以看出Javaq_由Java虚拟机和Java应用E序接口搭徏QJava语言则是q入q个q_的通道Q用Java语言~写q编译的E序可以q行在这个^C。这个^台的l构如下图所C:
在Javaq_的结构中, 可以看出QJava虚拟?JVM) 处在核心的位|,是程序与底层操作pȝ和硬件无关的关键。它的下ҎUL接口Q移植接口由两部分组成:适配器和Java操作pȝ, 其中依赖于^台的部分UCؓ适配器;JVM 通过UL接口在具体的q_和操作系l上实现Q在JVM 的上ҎJava的基本类库和扩展cd以及它们的APIQ?利用Java API~写的应用程?application) 和小E序(Java applet) 可以在Q何Javaq_上运行而无需考虑底层q_, 是因ؓ有Java虚拟?JVM)实现了程序与操作pȝ的分,从而实CJava 的^台无x?
那么到底什么是Java虚拟?JVM)呢?通常我们谈论JVMӞ我们的意思可能是Q?
- 对JVM规范的的比较抽象的说明;
- 对JVM的具体实玎ͼ
- 在程序运行期间所生成的一个JVM实例?
对JVM规范的的抽象说明是一些概늚集合Q它们已l在书《The Java Virtual Machine Specification》(《Java虚拟范》)中被详细地描qCQ对JVM的具体实现要么是软gQ要么是软g和硬件的l合Q它已经被许多生产厂商所实现Qƈ存在于多U^C上;q行JavaE序的Q务由JVM的运行期实例单个承担。在本文中我们所讨论的Java虚拟?JVM)主要针对W三U情况而言。它可以被看成一个想象中的机器,在实际的计算Z通过软g模拟来实玎ͼ有自己想象中的硬Ӟ如处理器、堆栈、寄存器{,q有自己相应的指令系l?
JVM在它的生存周期中有一个明的dQ那是q行JavaE序Q因此当JavaE序启动的时候,׃生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。下面我们从JVM的体pȝ构和它的q行q程q两个方面来对它q行比较深入的研I?
2 Java虚拟机的体系l构
刚才已经提到QJVM可以׃同的厂商来实现。由于厂商的不同必然DJVM在实C的一些不同,然而JVMq是可以实现跨^台的Ҏ,q就要归功于设计JVM时的体系l构了?
我们知道Q一个JVM实例的行Z光是它自q事,q涉及到它的子系l、存储区域、数据类型和指oq些部分Q它们描qCJVM的一个抽象的内部体系l构Q其目的不光规定实现JVM时它内部的体pȝ构,更重要的是提供了一U方式,用于严格定义实现时的外部行ؓ。每个JVM都有两种机制Q一个是装蝲h合适名U的c?cL是接?Q叫做类装蝲子系l;另外的一个负责执行包含在已装载的cL接口中的指oQ叫做运行引擎。每个JVM又包括方法区、堆、Java栈、程序计数器和本地方法栈q五个部分,q几个部分和c装载机制与q行引擎机制一L成的体系l构图ؓQ?
? JVM的体pȝ?
JVM的每个实例都有一个它自己的方法域和一个堆Q运行于JVM内的所有的U程都共享这些区域;当虚拟机装蝲cL件的时候,它解析其中的二进制数据所包含的类信息Qƈ把它们放到方法域中;当程序运行的时候,JVM把程序初始化的所有对象置于堆上;而每个线E创建的时候,都会拥有自己的程序计数器和Java栈,其中E序计数器中的值指向下一条即被执行的指令,U程的Java栈则存储U程调用JavaҎ的状态;本地Ҏ调用的状态被存储在本地方法栈Q该Ҏ栈依赖于具体的实现?
下面分别对这几个部分q行说明?
执行引擎处于JVM的核心位|,在Java虚拟范中Q它的行为是由指令集所军_的。尽对于每条指令,规范很详l地说明了当JVM执行字节码遇到指令时Q它的实现应该做什么,但对于怎么做却a之甚。Java虚拟机支持大U?48个字节码。每个字节码执行一U基本的CPUq算,例如,把一个整数加到寄存器,子程序{Uȝ。Java指o集相当于JavaE序的汇~语a?
Java指o集中的指令包含一个单字节的操作符,用于指定要执行的操作,q有0个或多个操作?提供操作所需的参数或数据。许多指令没有操作数,仅由一个单字节的操作符构成?
虚拟机的内层循环的执行过E如?
do{
取一个操作符字节;
Ҏ操作W的值执行一个动?
}while(E序未结?
׃指opȝ的简单?使得虚拟机执行的q程十分?从而有利于提高执行的效率。指令中操作数的数量和大是由操作符军_的。如果操作数比一个字节大,那么它存储的序是高位字节优先。例?一?6位的参数存放时占用两个字?其gؓ:
W一个字?256+W二个字节字节码?
指o一般只是字节对齐的。指令tableswitch和lookup是例?在这两条指o内部要求强制?字节边界寚w?
对于本地Ҏ接口Q实现JVMq不要求一定要有它的支持,甚至可以完全没有。Sun公司实现Java本地接口(JNI)是出于可UL性的考虑Q当然我们也可以设计出其它的本地接口来代替Sun公司的JNI。但是这些设计与实现是比较复杂的事情Q需要确保垃圑֛收器不会那些正在被本地Ҏ调用的对象释放掉?
Java的堆是一个运行时数据?cȝ实例(对象)从中分配I间Q它的管理是由垃圑֛收来负责?不给E序员显式释攑֯象的能力。Java不规定具体用的垃圾回收法,可以Ҏpȝ的需求用各U各L法?
JavaҎZ传统语言中的~译后代码或是Unixq程中的正文D늱伹{它保存Ҏ代码(~译后的java代码)和符可。在当前的Java实现?Ҏ代码不包括在垃圾回收堆中,但计划在来的版本中实现。每个类文g包含了一个JavacL一个Java界面的编译后的代码。可以说cL件是Java语言的执行代码文件。ؓ了保证类文g的^台无x?Java虚拟范中对类文g的格式也作了详细的说明。其具体l节请参考Sun公司的Java虚拟范?
Java虚拟机的寄存器用于保存机器的q行状?与微处理器中的某些专用寄存器cM。Java虚拟机的寄存器有四种:
- pc: JavaE序计数器;
- optop: 指向操作数栈端的指针;
- frame: 指向当前执行Ҏ的执行环境的指针Q?
- vars: 指向当前执行Ҏ的局部变量区W一个变量的指针?
在上qCpȝ构图中,我们所说的是第一U,即程序计数器Q每个线E一旦被创徏拥有了自己的程序计数器。当U程执行JavaҎ的时候,它包含该U程正在被执行的指o的地址。但是若U程执行的是一个本地的ҎQ那么程序计数器的值就不会被定义?
Java虚拟机的栈有三个区域:局部变量区、运行环境区、操作数区?
局部变量区
每个JavaҎ使用一个固定大的局部变量集。它们按照与vars寄存器的字偏U量来寻址。局部变量都?2位的。长整数和双_ֺ点数占据了两个局部变量的I间,却按照第一个局部变量的索引来寻址?例如,一个具有烦引n的局部变?如果是一个双_ֺ点?那么它实际占据了索引n和n+1所代表的存储空?虚拟范ƈ不要求在局部变量中?4位的值是64位对齐的。虚拟机提供了把局部变量中的D载到操作数栈的指?也提供了把操作数栈中的值写入局部变量的指o?
q行环境?
在运行环境中包含的信息用于动态链?正常的方法返回以及异常捕捉?
动态链?/b>
q行环境包括Ҏ向当前类和当前方法的解释器符可的指?用于支持Ҏ代码的动态链接。方法的class文g代码在引用要调用的方法和要访问的变量时用符受动态链接把W号形式的方法调用翻译成实际Ҏ调用,装蝲必要的类以解释还没有定义的符?q把变量讉K译成与q些变量q行时的存储l构相应的偏Ud址。动态链接方法和变量使得Ҏ中用的其它cȝ变化不会影响到本E序的代码?
正常的方法返?/b>
如果当前Ҏ正常地结束了,在执行了一条具有正类型的q回指o?调用的方法会得到一个返回倹{执行环境在正常q回的情况下用于恢复调用者的寄存?q把调用者的E序计数器增加一个恰当的数?以蟩q已执行q的Ҏ调用指o,然后在调用者的执行环境中l执行下厅R?
异常捕捉
异常情况在Java中被UCError(错误)或Exception(异常),是Throwablecȝ子类,在程序中的原因是:①动态链接错,如无法找到所需的class文g。②q行旉,如对一个空指针的引用。程序用了throw语句?
当异常发生时,Java虚拟机采取如下措?
- 查与当前Ҏ相联pȝcatch子句表。每个catch子句包含其有效指令范?能够处理的异常类?以及处理异常的代码块地址?
- 与异常相匚w的catch子句应该W合下面的条?造成异常的指令在其指令范围之?发生的异常类型是其能处理的异常类型的子类型。如果找C匚w的catch子句,那么pȝ转移到指定的异常处理块处执行;如果没有扑ֈ异常处理?重复L匚w的catch子句的过E?直到当前Ҏ的所有嵌套的catch子句都被查过?
- ׃虚拟ZW一个匹配的catch子句处l执?所以catch子句表中的顺序是很重要的。因为Java代码是结构化?因此d以把某个Ҏ的所有的异常处理器都按序排列C个表?对Q意可能的E序计数器的?都可以用U性的序扑ֈ合适的异常处理?以处理在该程序计数器g发生的异常情c?
- 如果找不到匹配的catch子句,那么当前Ҏ得到一?未截获异?的结果ƈq回到当前方法的调用?好像异常刚刚在其调用者中发生一栗如果在调用者中仍然没有扑ֈ相应的异常处理块,那么q种错误被传播下去。如果错误被传播到最层,那么pȝ调用一个缺省的异常处理块?
操作数栈?
机器指o只从操作数栈中取操作?对它们进行操?q把l果q回到栈中。选择栈结构的原因?在只有少量寄存器或非通用寄存器的机器(如Intel486)?也能够高效地模拟虚拟机的行ؓ。操作数栈是32位的。它用于l方法传递参?q从Ҏ接收l果,也用于支持操作的参数,q保存操作的l果。例?iadd指o两个整数相加。相加的两个整数应该是操作数栈顶的两个字。这两个字是由先前的指o压进堆栈的。这两个整数从堆栈弹出、相?q把l果压回到操作数栈中?
每个原始数据cd都有专门的指令对它们q行必须的操作。每个操作数在栈中需要一个存储位|?除了long和double?它们需要两个位|。操作数只能被适用于其cd的操作符所操作。例?压入两个intcd的数,如果把它们当作是一个longcd的数则是非法的。在Sun的虚拟机实现?q个限制由字节码验证器强制实行。但?有少数操?操作Wdupe和swap),用于对运行时数据行操作时是不考虑cd的?
本地Ҏ栈,当一个线E调用本地方法时Q它׃再受到虚拟机关于l构和安全限制方面的U束Q它既可以访问虚拟机的运行期数据区,也可以用本地处理器以及Mcd的栈。例如,本地栈是一个C语言的栈Q那么当CE序调用C函数Ӟ函数的参C某种序被压入栈Q结果则q回l调用函数。在实现Java虚拟机时Q本地方法接口用的是C语言的模型栈Q那么它的本地方法栈的调度与使用则完全与C语言的栈相同?
3 Java虚拟机的q行q程
上面对虚拟机的各个部分进行了比较详细的说明,下面通过一个具体的例子来分析它的运行过E?
虚拟机通过调用某个指定cȝҎmain启动Q传递给main一个字W串数组参数Q指定的类被装载,同时链接该类所使用的其它的cdQƈ且初始化它们。例如对于程序:
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]);
}
}
}
~译后在命o行模式下键入Q?java HelloApp run virtual machine
通过调用HelloApp的方法main来启动java虚拟机,传递给main一个包含三个字W串"run"?virtual"?machine"的数l。现在我们略q虚拟机在执行HelloApp时可能采取的步骤?
开始试图执行类HelloApp的mainҎQ发现该cdƈ没有被装载,也就是说虚拟机当前不包含该类的二q制代表Q于是虚拟机使用ClassLoader试图Lq样的二q制代表。如果这个进E失败,则抛Z个异常。类被装载后同时在mainҎ被调用之前,必须对类HelloApp与其它类型进行链接然后初始化。链接包含三个阶D:验,准备和解析。检验检查被装蝲的主cȝW号和语义,准备则创建类或接口的静态域以及把这些域初始化ؓ标准的默认|解析负责查主cd其它cL接口的符号引用,在这一步它是可选的。类的初始化是对cM声明的静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。整个过E如下:
?Q虚拟机的运行过E?
4 l束?
本文通过对JVM的体pȝ构的深入研究以及一个JavaE序执行时虚拟机的运行过E的详细分析Q意在剖析清楚Java虚拟机的机理?

]]>