MiniJavaVM——一个Java虚拟机的设计和实现

http://ba5ag.zrsa.org/paper/ZhuHuaiyi.html


MiniJavaVM——一个Java虚拟机的设计和实现

摘要

本文叙述了Java虚拟机(JVM)的概念及如何设计和实现一个Java虚拟机——MiniJavaVM。着重介绍了虚拟机的体系结构及如何设计和实现这个体系结构。在探讨虚拟机的设计过程中详细介绍了MiniJavaVM虚拟机各部分的设计,包括类的装载和解析,内存管理,执行引擎,方法调用和异常处理部分。最后通过测试MiniJavaVM来验证设计和实现的正确性。

关键词

Java虚拟机(JVM) 字节码 类装载 执行引擎 本地方法


MiniJavaVM – a design and implementation of a Java Virtual Machine

Abstract

This paper describes the conception of Java Virtual Machine (JVM) and how to design and implement a Java Virtual Machine – MiniJavaVM. It emphasizes the architecture of JVM and how to design and implement the architecture. It describes the details about each part of JVM when discussing how to design the JVM, including class-loading and resolution, memory management, execution engine, method invoking and exception-handling. At last, the correctness of the design and implementation is validated by testing MiniJavaVM.

Keywords

Java Virtual Machine(JVM), byte code, Class-loading, execution engine, Native Method





目 录

第一章 绪论... 6

1.1 Java及Java虚拟机... 6

1.2 Java虚拟机的体系结构... 7

1.3 MiniJavaVM的功能... 10

1.4 MiniJavaVM的运行环境及开发工具... 12

第二章 系统设计... 13

2.1 唯一的虚拟机MiniJavaVM... 13

2.2 MiniJavaVM的构成要素... 13

2.2.1 虚拟机总体框架... 13

2.2.2 命令参数解析模块... 15

2.2.3 类的装载和解析模块... 15

2.2.4 内存管理模块... 16

2.2.5 执行引擎模块... 17

2.2.6 方法调用模块... 18

2.2.7 异常处理模块... 19

第三章 虚拟机框架的实现... 21

3.1 JVM工程... 22

3.2 JavaVM工程... 23

3.2.1 Java虚拟机的数据类型和字长考量... 23

3.2.2 JavaVM类... 24

3.3 JavaNativeCall工程... 29

第四章 类的装载和解析... 30

4.1 Java Class文件... 30

4.2 Class文件在MiniJavaVM中的数据结构表示... 32

4.3 类的装载和解析... 36

第五章 内存管理... 41

5.1 对象、堆、方法区的管理... 41

5.2 MiniJavaVM的垃圾回收过程... 44

第六章 执行引擎——Java操作码实现... 47

6.1 Java虚拟机中的操作码功能分类... 47

6.2 操作码功能实现——JavaOperatorExecute类... 49

第七章 方法调用的实现... 50

7.1 Java中的方法调用... 50

7.2 非本地方法的实现... 51

7.3 本地方法的实现... 52

7.3.1 Java中的本地方法... 52

7.3.2 NativeMethod_access类... 54

7.3.3 如何处理本地方法可变参数问题... 54

第八章 异常的实现... 56

8.1 Java中的异常... 56

8.2 异常在MiniJavaVM中的实现... 56

第九章 验证MiniJavaVM的正确性... 58

9.1 MiniJavaVM的使用方法... 58

9.2 测试操作码实现的正确性... 61

9.2.1 方法调用的正确性... 61

9.2.2 数学运算的正确性... 64

9.2.3 控制流语句的正确性... 65

9.3 测试本地方法调用... 66

9.4 测试异常处理... 67

第十章 不足与后续工作... 69

10.1 本地方法... 69

10.2 I/O操作... 69

10.3 多线程... 69

10.4 效率... 69

致谢... 71

参考文献... 72





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

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


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

什么是Java虚拟机?Java虚拟机是运行所有Java程序的抽象计算机,它仅仅是由一个规范来定义的抽象的计算机。当提及“Java虚拟机”时,可能指的是如下三种不同的东西:

Ÿ 抽象规范

Ÿ 一个具体的实现

Ÿ 一个运行中的虚拟机实例[2]

Java虚拟机负责Java程序设计语言的内存安全、平台无关和安全特性。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机(JVM)在多个平台上实现统一语言。Java之所以得以大行其道,除了它是一门面向对象、构造精美的语言之外,更重要的原因在于:它摆脱了具体机器的束缚,使跨越不同平台编写程序成为可能。

1.2 Java虚拟机的体系结构
在Java虚拟机规范中,一个虚拟机实例的行为是分别按照子系统、内存区、数据类型以及指令这几个术语来描述的。这些组成部分一起展示了抽象的虚拟机的内部抽象体系结构。但是规范中对它们的定义并非是要强制规定Java虚拟机实现内部的体系结构,更多的是为了严格地定义这些实现的内部特征。规范本身通过定义这些抽象的组成部分以及它们之间的交互,来定义任何Java虚拟机实现都必须遵守的行为。每个JVM都有两种机制,一个是装载具有合适名称的类(类或是接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。每个JVM又包括方法区、堆、Java栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一起组成了Java虚拟机的体系结构。

图1.2.1描述了Java虚拟机的结构框图,包括在规范中描述的主要子系统和内存区。


每个Java虚拟机都有一个类装载器子系统,它根据给定的全限定名来装入类型(类或接口)。同样,每个Java虚拟机都有一个执行引擎,它负责执行那些包含在被装载类的方法中的指令。

Java虚拟机的运行时数据区存储了许多运行时数据,例如,字节码,从已装载的class文件中得到的其他信息,程序创建的对象,传递给方法的参数,返回值,局部变量,以及运算的中间结果等。Java虚拟机把这些东西都组织到几个“运行时数据区”中,以便于管理。

某些运行时数据区是由程序中所有线程共享的,还有一些则只能由一个线程拥有。每个Java虚拟机实例都有一个方法区及一个堆,它们是由该虚拟机实例中所有线程共享的。当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息。然后把这些类型信息放到方法区中。当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。图1.2.2描述了这些内存区域。


当每一个线程被创建时,它都将得到它自己的PC寄存器以及一个Java栈。如果线程正在执行的是一个Java方法(非本地方法),那么PC寄存器的值将总是指示下一条将被执行的指令,而它的Java栈则总是存储该线程中Java方法调用的状态——包括它的局部变量,被调用时传进来的参数,它的返回值,以及运算的中间结果等。而本地方法调用的状态,则是以某种依赖于具体实现的方式存储在本地方法栈中,也可能是在寄存器或其他某些与特定实现相关的内存区中。

Java栈是由许多栈帧(stack frame)组成的,一个栈帧包含一个Java方法调用的状态。当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中;当该方法返回时,这个栈帧被从Java栈中弹出并抛弃。

Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。这样设计的原因是为了保持Java虚拟机的指令集尽量紧凑,同时也便于Java虚拟机在那些只有很少通用寄存器的平台上实现。另外,Java虚拟机的这种基于栈的体系结构,也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。

图1.2.3描绘了Java虚拟机为每个线程创建的内存区,这些内存区是私有的,任何线程都不能访问另一个线程的PC寄存器或者Java栈。[3]


1.3 MiniJavaVM的功能
² 能够装载并解析java class文件

对于已经编译好的java class文件,能够读取该class文件的内容,装载该类,并保存在程序内部的数据结构中。当在程序运行的过程中需要解析该类时,进行解析,并替换符号引用为直接引用

² 在完成虚拟机的初始化后,能够找到main函数并执行程序[13]

对于指定的入口类,在虚拟机完成了初始化后,寻找该类的main()方法,如果找到,则执行该方法,否则抛出异常,虚拟机运行中止。

² 支持Java虚拟机规范中规定的200多个操作码的功能

实现了Java虚拟机的200多个操作码的功能,由此使MiniJavaVM这个虚拟机模拟Java虚拟机的功能成为可能,这200多个操作码包括:

Ø 栈和局部变量操作指令

ü 将常量池入指令

ü 从栈中的局部变量中装载值指令

ü 将栈中的值存入局部变量指令

ü 通用栈操作指令

Ø 类型转换指令

Ø 整数运算指令

Ø 逻辑运算指令

ü 移位操作指令

ü 按位布尔运算指令

Ø 浮点运算指令

Ø 对象和数组指令

ü 对象操作指令

ü 数组操作指令

Ø 控制流指令

ü 条件分支指令

ü 比较指令

ü 无条件转移指令

ü 表跳转指令

Ø 异常指令

Ø finally子句指令

Ø 方法调用与返回指令

ü 方法调用指令

ü 方法返回指令

Ø 线程同步指令

² 具有内存管理和垃圾收集机制

Java虚拟机对内存的管理使得java程序具有很高的安全性,程序员不用担心内存访问越界问题,也不用为在合适的时候释放分配的空间而费心。垃圾收集机制的存在解决何时回收不用的内存和如何回收内存的问题。

² 支持非本地方法调用

按照Java虚拟机规范中的要求来设置非本地方法的调用情况,包括参数压栈,分配局部变量空间,压入方法调用的栈桢等。

² 支持本地方法调用

Java虚拟机中所有与本地方法相关的部分都重新写过,以动态链接库的形式为MiniJavaVM工程提供支持。MiniJavaVM 的本地方法只实现最基本的功能,不再负责虚拟机的安全机制。

² 支持异常处理

有了异常处理,就能够在程序运行时平稳处理意外情况。根据Java class文件中的异常表,MiniJavaVM程序支持所有的异常处理,并在不能解决异常时输出异常信息,虚拟机停止运行。

² 能够运行与I/O无关的完整Java程序,并提供参数供查看运行效果

提供了-version,-showversion,–help,-?,–verbose 命令。

-version命令显示MiniJavaVM的版本信息,然后退出

-showversion命令显示MiniJavaVM的版本信息,然后继续运行Java程序

-help,-?命令显示帮助信息

-verbose命令输出详细数据显示运行过程

1.4 MiniJavaVM的运行环境及开发工具
开发平台:Windows XP/2003

开发语言:ANSI C/C++

开发工具:Visual Studio C++/ Visual Studio.net

运行平台:Windows XP/2000/2003





第二章 系统设计
2.1 唯一的虚拟机MiniJavaVM
一个运行的Java虚拟机实例的天职就是:负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果在同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序实例都运行于它自己的Java虚拟机实例中。[2]

为此,需要有一种机制保证在运行过程中只有一个Java虚拟机的实例产生。首先要定义表示Java虚拟机的类,然后保证这个类有只一个实例。一种可以供选择的方案是使用Singleton设计模式,保证Java虚拟机实例在整个程序运行过程中只有一个;另一种是可以考虑MFC(Microsoft Fundamental Class)中唯一的全局变量theApp来表示程序运行的实例,通过AfxGetApp()的方法来得到该唯一的全局变量的指针,然后在此基础上进行操作。

在这里,我们选择MFC方式定义唯一的全局变量来表示Java虚拟机的运行实例,可以在程序进入main()函数前完成虚拟机的初始化。通过定义一个全局函数来返回虚拟机实例的指针。

2.2 MiniJavaVM的构成要素
2.2.1 虚拟机总体框架
我们的MiniJavaVM的框架合理地组织了虚拟机运行时所需的各模块,将各模块的输入与输出有效地结合在一起,使这些模块组合在一起完成了Java虚拟机的功能。这些模块包括:命令参数解析模块、类的装载和解析模块、内存管理模块、执行引擎模块、方法调用模块、异常处理模块、多线程处理模块(未完成)。我们的MiniJavaVM总的组织方式如图2.2.1所示。


其中除命令参数解析模块外,其他模块一起构成了完整的MiniJavaVM虚拟机,这些模块之间协同合作,完成了虚拟机的功能。其中命令参数解析模块负责解析命令,根据MiniJavaVM后的参数来设定虚拟机的运行模式及输出信息;类的装载和解析模块能从class文件或是rt.jar文件中装载指定名称的Java类,并采用迟解析的方式在需要时解析该类,类的信息维护在虚拟机的一个数据结构中;内存管理模块负责为类的实例及静态字段分配空间,并在虚拟机内维护类的实例和静态字段,当虚拟机空间不足时会启动垃圾回收机制来回收内存;执行引擎模块负责解释执行200多个操作码,解释的过程包括对栈桢、栈、PC、局部变量区的修改;多线程处理模块负责维护虚拟机内的表示线程的数据结构,在语言级提供多线程支持;方法调用模块负责处理方法调用过程,对于非本地方法,包括找到调用方法的指针,新建栈桢、将方法参数设置在新栈桢的局部变量区,调用方法并将返回值压栈的过程,对于本地方法,包括找到调用方法的指针,将方法参数用汇编的方式压栈,调用本地方法并将返回值压栈的过程;异常处理模块负责处理虚拟机抛出的异常,记录异常产生处的异常信息,并试图通过查找当前方法的异常表来处理异常信息,如果能够通过异常表找到处理异常的代码,则修改PC的值使虚拟机处理当前异常,否则,当虚拟机不能处理该异常时,输出异常信息,然后终止虚拟机的运行。通过这几个模块的协同合作,我们的MiniJavaVM虚拟机能够很好地模拟Java虚拟机的功能。

2.2.2 命令参数解析模块
命令参数解析模块负责解析命令行,根据MiniJavaVM后的参数来设定虚拟机的运行模式及输出信息。该模块的设计如图2.2.2所示:


命令参数解析模块在解析完命令行参数后,通过得到虚拟机的唯一实例的指针调用设置参数的方法来设置虚拟机运行时的参数。

2.2.3 类的装载和解析模块
类的装载和解析模块负责从java class文件或是rt.jar文件中装载指定名称的Java类,并采用迟解析的方式在需要时解析该类,类的信息维护在虚拟机的一个数据结构中。我们的MiniJavaVM类的装载和解析模块设计如图2.2.3所示:


其中要装载的类文件有两种渠道获得,一种是直接查找相应类的class文件,一种是从rt.jar文件中得到类的class文件的数据,为了统一这两种方式,可以先从这两种方法中生成类文件的字节流,再交由下一步程序处理。当生成class文件的字节流后,通过指定的模块读取字节流中的信息,生成该Java类在虚拟机中对应的数据。这样,类装载的部分算是完成。在虚拟机运行的过程中,会需要解析类中的常量池,将符号引用替换为直接引用。

2.2.4 内存管理模块
内存管理模块负责为类的实例及静态字段分配空间,并在虚拟机内维护类的实例和静态字段,当虚拟机空间不足时会启动垃圾回收机制来回收内存。

在Java虚拟机中,关于被装载类型的信息存储在一个逻辑上称为方法区的内存中,所有线程共享方法区,因此它们对方法区数据的访问必须被设计为是线程安全的。方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整。同样,方法区也不必是连续的,方法区可以在一个堆(甚至是虚拟机自己的堆)中自由分配。另外,虚拟机也可以允许用户或者程序员指定方法区的初始大小以及最小和最大尺寸等。[4]

方法区也可以被垃圾收集。因为虚拟机允许用户定义的类装载器来动态扩展Java程序,因此一些类也会成为程序“不再引用”的类。当某个类变为不再被引用的类时,Java虚拟机可以卸载这个类(垃圾收集),从而使方法区占据的内存保持最小。

为了简单,我们的MiniJavaVM的方法区使用虚拟机自己的堆,不参与垃圾回收,同时,类的静态字段及一些特殊的类的实例(如与每个类相关的Class类的实例)也不参与垃圾回收。

我们的MiniJavaVM内存管理模块设计如图2.2.4所示:


当MiniJavaVM虚拟机请求一个可以被回收的内存空间(比如普通类的实例空间)时,则通过内存管理模块分配可回收的内存,所分配的内存的实地址经过映射后返回给虚拟机,同时已分配的内存地址记录在一张哈希表中,供快速查找所用。如果虚拟机已经没有可分配的空间,则运行垃圾收集,垃圾收集完成后再分配内存。当虚拟机请求一个不可被回收的内存空间(比如类的静态字段空间,表示已装载类的Class类的实例等)时,通过内存管理模块分配不可回收内存,所分配的内存的实地址经过映射后返回给虚拟机,同时已分配的内存地址记录在一张哈希表中,供快速查找所用。当垃圾收集完成后仍没有可分配的内存地址可用时,虚拟机退出。

2.2.5 执行引擎模块
任何Java虚拟机实现的核心都是它的执行引擎。在Java虚拟机规范中,执行引擎的行为使用指令集来定义。对于每条指令,Java虚拟机规范都详细规定了当实现执行到该指令时应该处理什么。[5]执行引擎模块负责解释执行Java200多个操作码,解释的过程包括对Java栈桢、Java栈、PC、局部变量区的修改。我们的MiniJavaVM执行引擎模块设计如图2.2.5所示:


在调用执行引擎模块时,会传入一字节码流,执行引擎模块负责解释这一字节码流。在解释字节码过程中,Java虚拟机可能会装载新的Java类,分配某个Java类的实例,进行数学运算,修改当前java栈桢、Java栈、PC、局部变量区等。

2.2.6 方法调用模块
方法调用模块负责处理方法调用过程,对于非本地方法,包括找到调用方法的指针,新建栈桢、将方法参数设置在新栈桢的局部变量区,调用方法并将返回值压栈的过程,对于本地方法,包括找到调用方法的指针,将方法参数用汇编的方式压栈,调用本地方法并将返回值压栈的过程。我们的MiniJavaVM方法调用模块的设计如图2.2.6所示:


虚拟机为每一个调用的Java(非本地)方法一个新的栈帧。栈帧包括:为方法的局部变量所预留的空间,该方法的操作数栈,以及特定虚拟机实现需要的其他所有信息。局部变量和操作数栈的大小在编译时计算出来,并放置到class文件中去,然后虚拟机就能够了解到方法的栈帧需要多少内存。当虚拟机调用一个方法的时候,它为该方法创建恰当大小的栈帧,再将新的栈帧压入Java栈。处理实例方法时,虚拟机从所调用方法栈帧内的操作数栈中弹出objectref和args。虚拟机把objectref作为全局变量0放到新的栈帧中,把所有的args作为局部变量1,2,……等处理。Objectref是隐式传给所有实例方法的this指针。对于类方法,虚拟机只从所调用的方法栈帧中的操作数栈中弹出参数,并将它们放到新的栈帧中去作为局部变量0,1,2……当objectref和args(对于类方法则只有args)被赋给新栈帧中的局部变量后,虚拟机把新的栈帧作为当前栈帧,然后将程序计数器指向新方法的第一条指令。[6]

虚拟机使用一种“与实现相关”的风格调用本地方法。当调用本地方法时,虚拟机不会将一个新的栈帧压入Java栈。当线程进入到本地方法的那一刻,它就将Java栈抛在身后。直到本地方法返回以后,Java栈才被重新调用。这里本虚拟机的实现将使用调用动态链接库中的方法来实现本地方法调用的过程。

2.2.7 异常处理模块
异常处理模块负责处理虚拟机抛出的异常,记录异常产生处的异常信息,并试图通过查找当前方法的异常表来处理异常信息,如果能够通过异常表找到处理异常的代码,则修改PC的值使虚拟机处理当前异常,否则,当虚拟机不能处理该异常时,输出异常信息,然后终止虚拟机的运行。我们的MiniJavaVM异常处理模块的设计如图2.2.7所示:





第三章 虚拟机框架的实现
Java虚拟机的体系结构如图3.1所示:


实现虚拟机框架,需要考虑以下几点:

² 虚拟机的数据类型和字长

² 如何实现栈结构

² 如何装载和解析类

² 如何调用本地和非本地方法

² 如何实现执行引擎

² 如何实现多线程

² 机如何组织方法区和堆

² 如何进行垃圾回收

² 虚拟机如何处理异常

为了解决这些问题,这里我们把MiniJavaVM虚拟机的实现总体分为三部分。

第一部分为主体部分,JavaVM工程。这个工程相当于实现了一个Java虚拟机的所有功能,包括定义虚拟机的数据类型和字长,实现虚拟机栈结构,组织方法区和堆,装载和解析类,实现执行引擎,调用非本地方法,实现多线程,处理异常等。但这个工程不负责虚拟机在执行过程中需要调用的本地方法的实现。由于本地方法总是与Java虚拟机实现的底层平台相关的,因此这个工程只负责处理与底层平台实现不相关的部分,而将与底层平台实现相关的部分交给另外一个工程JavaNativeCall工程。JavaVM工程将开发为动态链接库的形式,有利于别的工程调用这个工程中重要的导出类和导出方法。

第二部分为本地方法实现部分,JavaNativeCall工程。此工程负责本地方法接口(JNI)的声明和实现。该工程将开发为动态链接库的形式,有利于JavaVM工程调用该工程中的导出函数。

第三部分为主程序,JVM工程。该工程负责解析并设置虚拟机运行参数,调用JavaVM工程的虚拟机实现类来启动虚拟机。该工程为可执行文件。

我们将主程序与虚拟机工程分离,底层平台实现相关与实现无关部分代码分离,使MiniJavaVM虚拟机的实现更具层次感。同时,也具有了更好的可扩充性

3.1 JVM工程
JVM工程负责解析并设置虚拟机运行参数,调用JavaVM工程的虚拟机实现类来启动虚拟机。JVM工程所要完成的任务可以用图3.1.1表示:


JVM工程只是调用JavaVM工程中导出的JavaVM类,设置其参数及入口类,所有Java虚拟机的运行工程交由JavaVM类完成。

3.2 JavaVM工程
JavaVM工程实现了一个Java虚拟机的所有功能,包括定义虚拟机的数据类型和字长,实现虚拟机栈结构,组织方法区和堆,装载和解析类,实现执行引擎,调用非本地方法,实现多线程,处理异常等。但这个工程不负责虚拟机在执行过程中需要调用的本地方法的实现。由于本地方法总是与Java虚拟机实现的底层平台相关的,因此这个工程只负责处理与底层平台实现不相关的部分,而将与底层平台实现相关的部分交给另外一个工程JavaNativeCall工程。

3.2.1 Java虚拟机的数据类型和字长考量
Java虚拟机是通过某些数据类型来执行计算的,数据类型及其运算都是由Java虚拟机规范严格定义的。数据类型可以分为两种:基本类型和引用类型。基本类型的变量持有原始值,而引用类型的变量持有引用值,图3.2.1描述了Java虚拟机中的数据类型:


Java虚拟机规范定义了每一种数据类型的取值范围,但是没有定义它们的位宽。存储这些类型的值所需的占位宽度,是由具体的虚拟机实现的设计者决定的。Java语言中的所有基本类型同样也是Java虚拟机中的基本类型。但boolean有点特别,当编译器把Java源码编译为字节码时,它会用int或byte来表示boolean。在Java虚拟机中,false是由整数零来表示的,所有非零整数都表示true。涉及boolean值的操作则会使用int。

表3.2.1 Java虚拟机数据类型的取值范围

类型
范围

Byte
8比特,带符号,二进制补码

Short
16比特,带符号,二进制补码

Int
32比特,带符号,二进制补码

Long
64比特,带符号,二进制补码

Char
16比特,不带符号,Unicode字符

Float
32比特,IEEE754标准单精度浮点数

Double
64比特,IEEE754标准单精度浮点数

ReturnAddress
同一方法中某操作码的地址

Reference
堆中对某对象的引用,或者是null


Java虚拟机中,最基本的数据单元就是字(word),它的大小是由每个虚拟机实现的设计者来决定的。字长必须足够大,至少是一个字单元就足以持有byte、short、int、char、float、returnAddress或者reference类型的值,而两个字单元就足以持有long或者double类型的值。因此,虚拟机实现的设计者至少得选择32位作为字长,或者选择更为高效的字长大小。通常根据底层主机平台的指针长度来选择字长。

Java虚拟机规范中,关于运行时数据区的大部分内容,都是基于“字”这个抽象概念的。比如,关于栈帧的两个部分——局部变量和操作数栈——都是按照“字”来定义的。这个内容区域能够容纳任何虚拟机数据类型的值,当把这些值放到局部变量或者操作数栈中时,它将占用一个或两个字单元。[7]

在我们的JavaVM工程中,对Java虚拟机的数据类型和字长的定义如下:


3.2.2 JavaVM类
JavaVM类是JavaVM工程中最重要的一个类,这个类定义了Java虚拟机应该实现的功能,所有Java虚拟机相关的功能对应于通过调用该类的相应方法来实现。

为了定义JavaVM类,我们必须定义其他一些辅助类。这些辅助类包括:

JavaThread类:负责管理虚拟机线程

CClassFile类:负责读取Java Class文件的内容

JavaClassInfo类:负责根据CClassFile类的内容生成该java类的类信息

JavaArrayInfo类:负责生成某一类型的数组信息

JavaClassFileLoader类:负责找出要装载的java class文件,并将文件内容作为二进制流传给CClassFile类

Cp_xxxxxxxx类:负责维护Class文件中常量池的内容

Attr_xxxxxxxx类:负责维护Class文件常量池中attr_info相关的内容

Memxxxxxxx类:负责虚拟机的内存管理,包括分配内存和进行垃圾收集

JavaVM类的接口定义如下:


我们的JavaVM类内部组织如图3.2.3所示:


其中线程部分设计如图3.2.4所示:


通过上面设计,使用JavaVM类来表示Java虚拟机,使用一个唯一的JavaVM的全局变量来代表运行中的Java虚拟机;使用链表来存储在Java虚拟机上运行的所有线程,并保留一个指向当前线程的指针;使用链表来存储已经装载的class文件的信息,并保留一个指向入口class的指针,以找到正确的main()入口;在JavaVM类中定义内存管理模块的接口,通过调用这些接口来分配并读写内存,并把类信息同相应的返回的引用值联系起来;在JavaVM类中定义垃圾收集模块的接口,通过这些接口来进行垃圾收集。

3.3 JavaNativeCall工程
JavaNativeCall工程为本地方法实现部分工程。此工程负责本地方法接口(JNI)的声明和实现。该工程开发为动态链接库的形式,有利于JavaVM工程调用该工程中的导出函数,真正做到本地方法调用的功能。

为了实现这个工程,这里参考了Kaffe中关于JNI实现部分的代码。Kaffe是一个功能强大的Java虚拟机,它包括Java2 平台的子集,标准Java API和工具来提供Java运行时环境。[12]MiniJavaVM的本地方法接口(JNI)部分主要是参考了Kaffe中对本地方法接口的定义。我们的MiniJavaVM的本地方法接口的实现定义了如下类型:


所有JNI函数的声明均按照“_MiniJavaVM+包名+类名+方法名+参数列表”的格式声明。如java.lang.System类的本地方法registerNatives()的声明如下:

java_void _MiniJava_java_lang_System_registerNatives(JavaVM* pJVM,java_class clsref);

具体调用时,只要在装载该方法所在的动态链接库后再得到该方法的地址,将参数压栈,调用这个方法,就能完成这个方法的调用,也就实现了Java本地方法。





第四章 类的装载和解析
4.1 Java Class文件
Java class文件中包含了Java虚拟机所需知道的、关于类或接口的所有信息。Java class文件是8位字节的二进制流。数据项按顺序存储在class文件中,相邻的项之间没有任何间隔,这样可以使class文件更紧凑。占据多个字节空间的项按照高位在前的顺序分为几个连续的字节存放。[8]

和Java类的可以包含多个不同的字段、方法、方法参数、局部变量等一样,Java class文件也能够包含许多不同大小的项。在class文件中,可变长度项的大小和长度位于其实际数据之前。这个特性使得class文件流可以从头到尾被顺序解析,首先读出项的大小,然后读出项的数据。

Class文件的基本类型如下,所有存储在u2,u4,u8项中的值,在class文件中以高位在前的形式出现。

表4.1.1 class文件“基本类型”

u1
1个字节,无符号类型

u2
2个字节,无符号类型

u4
4个字节,无符号类型

u8
8个字节,无符号类型


可变长度的ClassFile表中的项,如表4.1.2所示:

表4.1.2 ClassFile表的格式

类型
名称
数量

U4
Magic
1

U2
Minor_version
1

U2
Major_version
1

U2
Constant_pool_count
1

Cp_info
Constant_pool
Constant_pool_count-1

U2
Access_flags
1

U2
This_class
1

U2
Super_class
1

U2
Interfaces_count
1

U2
Interfaces
Interfaces_count

U2
Fields_count
1

Field_info
Fields
Fields_count

U2
Methods_count
1

Method_info
Methods
Methods_count

U2
Attributes_count
1

Attribute_info
Attributes
Attributes_count


Java Class文件中的常量池包括了与文件中类和接口相关的常量,其中存储了诸如文字字符串、final变量值、类名和方法名的常量。常量池中的许多入口都指向其他的常量池入口,而且class文件中紧随着常量池的许多条目也会指向常量池中的入口在整个class文件中,指示常量池入口在常量池列表中位置的整数索引都指向这些常量池入口。列表中的第一项索引值为1,第二项索引值为2,以此类推。尽管constant_pool列表中没有索引值为0的入口,但缺失的这一入口也被constant_pool_count计数在内。第个常量池入口都从一个长度为一个字节的标志开始,这个标志指出了列表中该位置的常量类型,一旦Java虚拟机获取并解析这个标志,Java虚拟机就会知道在标志后的常量类型是什么。表4.1.3列出了所有常量池标志的名字和值。

表4.1.3 常量池标志

入口类型
标志值
描述

CONSTANT_Utf8
1
UTF-8编码的Unicode字符串

CONSTANT_Integer
3
Int类型字面值

CONSTANT_Float
4
Float类型字面值

CONSTANT_Long
5
Long类型字面值

CONSTANT_Double
6
Double类型字面值

CONSTANT_Class
7
对一个类或接口的符号引用

CONSTANT_String
8
String类型字面值

CONSTANT_Fieldref
9
对一个字段的符号引用

CONSTANT_Methodref
10
对一个类中声明的方法的符号引用

CONSTANT_InterfaceMethodref
11
对一个接口中声明的方法的符号引用

CONSTANT_NameAndType
12
对一个字段或方法的部分符号引用


在类或者接口中声明的每一个字段(类变量或者实例变量)都由class文件中的一个名为field_info的可变长度的表进行描述。在一个class文件中,不会存在两个具有相同名字和描述的字段。表field_info的格式如下

表4.1.4 field_info表的格式

类型
名称
数量

U2
Access_flags
1

U2
Name_index
1

U2
Descriptor_index
1

U2
Attributes_count
1

Attribute_info
Attributes
Attributes_count


在class文件中,每个在类和接口中声明的方法,或者由编译器产生的方法,都由一个可变长度的method_info来描述。同一个类中不能存在两个名字及描述符完全相同的方法。有可能在class文件中出现的两种编译器产生的方法是:实例初始化方法(名为<init>)和类与接口初始化方法(名为<clinit>)。Method_info表的格式如下:

表4.1.5 method_info表的格式

类型
名称
数量

U2
Access_flags
1

U2
Name_index
1

U2
Descriptor_index
1

U2
Attributes_count
1

Attributes_info
Attributes
Attributes_count


属性在Java class文件中多处出现。它们可以出现在ClassFile、field_info、method_info和Code_attribute表中。Java虚拟机规范定义了9种属性,为了正确解释Java class文件,所有Java虚拟机实现都必须能够识别下列三种属性:Code,ConstatValue和Exception。为了正确地实现Java和Java2平台,虚拟机实现必须能够识别InnerClasses和Synthetic属性,但可以自主选择空间是识别还是忽略其他一些预定义的属性。由规范定义的attribute_info表的属性如下表:

表4.1.6 由规范定义的attribute_info表的类型

名称
使用者
描述

Code
Method_info
方法的字节码和其他数据

ConstantValue
Field_info
Fianl变量的值

Deprecated
Field_info、method_info
字段或者方法被禁用的指示符

Exceptions
Method_info
方法可能抛出的可被检测的异常

InnerClasses
ClassFile
内部、外部类的列表

LineNumberTable
Code_attribute
方法的行号与字节码的映射

LocalVariableTable
Code_attribute
方法的局部变量的描述

SourceFile
ClassFile
源文件名

Synthetic
Field_info、method_info
编译器产生的字段或者方法的指示符


4.2 Class文件在MiniJavaVM中的数据结构表示
JavaVM工程的CClassFile类负责读取java Class文件的内容,将class文件的二进制表示形式保存为虚拟机可以理解的数据结构。CClassFile类的设计如图4.2.1所示:


对于CClassFile类,最重要的就是读取指定Java类名的class文件,这个最重要的任务交给ReadClassFile()方法做。这个方法实现了三个功能:首先,找到该java类的class文件或是从rt.jar文件中找到该java类的class文件,读取这个类文件的二进制数据,并提交下一步处理;第二,根据二进制数据填充ClassFile类中的变量,这些变量的定义与ClassFile表一致;最后,根据ClassFile类的变量整理出类的信息,填充到新的结构JavaClassInfo中。Execute()函数对于入口class有用,通过调用该class文件的Execute()函数,Java虚拟机能从这个函数的main()函数开始执行。

ClassFile类的变量定义如图4.2.2所示:


Class文件常量池数据结构设计如下,所有的11种常量池表示类均从一个父类继承,并通过调用父类的静态成员函数GenerateCPInfo()来生成某一特殊类型的常量池:


在cp_info的静态成员函数GenerateCPInfo()中,先通过读取第一个字节来得知这个常量池的类型,然后根据这个类型来生成11个常量池类型中的一种,其类型值在m_nTag中保存,并返回它的指针(统一为cp_info的指针在ClassFile类中保存)。当要使用常量池中的某一值时,通过m_nTag的值将这一值转为正确的类型并使用。

Field_info类设计如下,这里必须保留ClassFile类的指针来获得对常量池信息的访问:


Method_info类设计如下,这里必须保留ClassFile类的指针来获得对常量池信息的访问,同时,需要保留一个attr_Code的指针来指向这个方法的执行码。


Attribute_info类设计如下,其中m_Type的值指定了这个attribute_info表示上述9种类型中的哪一种,ClassFile的指针用来访问这个class文件的常量池,m_pAttr这个attr_base的指针指向从attr_base派生出来的某一个子类。


Attr_base与其子类的关系同上面cp_info与其子类。如下图所示:


通过以上的设计和实现,Java Class文件的格式已经被我们的MiniJavaVM虚拟机完全表示出来。

4.3 类的装载和解析
在Java虚拟机中,负责查找并装载类型的那部分被称为类装载器子系统。Java虚拟机有两种类装载器:启动类装载器和用户自定义类型装载器。前者是Java虚拟机实现的一部分,后者则是Java程序的一部分。由不同的类装载器装载的类将被放在虚拟机内部的不同命名空间中。对于每一个被装载的类型,Java虚拟机都会为它创建一个java.lang.Class类的实例来代表该类型。和所有其他对象一样,用户自定义的类装载器以及Class类的实例都放在内存中的堆区,而装载的类型信息则都位为方法区。[7]

类装载器子系统除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。这些动作必须严格按以下顺序进行:

1) 装载——查找并装载类型的二进制数据

2) 连接——执行验证,准备,以及解析(可选)

验证 确保被导入类型的正确性

准备 为类变量分配内存,并将其初始化为默认值

解析 把类型中的符号引用转换为直接引用

3) 初始化——把类变量初始化为正确初始值


对于每一个被装载的类型,虚拟机都会在方法区中存储以下类型信息:

² 这个类型的全限定名

² 这个类型的直接超类的全限定名(除非这个类型是java.lang.Object,它没有超类)

² 这个类型是类类型还是接口类型

² 这个类型的访问修饰符(public、abstract或final的某个子集)

² 任何直接超接口的全限定名的有序列表

除了上面的基本类型信息外,虚拟机还得为每个被装载的类型存储以下信息:

² 该类型的常量池

² 字段信息

² 方法信息

² 除了常量以外的所有类(静态)变量

² 一个到类ClassLoader的引用

² 一个到Class类的引用

虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用常量的一个有序集合,包括直接常量(string、integer和floating point常量)和对其他类型、字段和方法的符号引用。池中的数据项就像数组一样是通过索引访问的。因为常量池存储了相应类型所用到的所有类型、字段和方法的符号引用,所以它在Java程序的动态连接中起着核心的作用。

对于类型中声明的每一个字段,方法区中必须保存下面的信息。除此之外,字段在类或者接口中的声明顺序也必须保存。下面是字段信息的清单:

² 字段名

² 字段的类型

² 字段的修饰符

对于类型中声明的每一个方法,方法区中必须保存下面的信息。和字段一样,这些方法在类或者接口中的声明顺序也必须保存。下面是方法信息的清单:

² 方法名

² 方法的返回类型(或void)

² 方法的数量和类型(按声明顺序)

² 方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)

除上面的清单中列出的条目之外,如果某个方法不是抽象的和本地的,它还必须保存下列信息:

² 方法的字节码(bytecodes)

² 操作数栈和该方法的栈帧中的局部变量区的大小

² 异常表

类变量是由所有类实例共享的,但是即使没有任何类实例,它也可以被访问。这些变量只与类有关——而非类的实例,因此它们总是作为类型信息的一部分而存储在方法区。除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须在方法区中为这些变量分配空间。而编译时常量(就是那些用final声明以及用编译时已知的值初始化的类变量)则和一般的类变量的处理方式不同,每个使用编译时常量的类型都会复制它的所有常量到自己的常量池中,或嵌入到它的字节码流中。作为常量池或字节码流的一部分,编译时常量保存在方法区中——就和一般的类变量一样。但是当一般的类变量作为声明它们的类型的一部分数据而保存的时候,编译时常量作为使用它们的类型的一部分而保存。

我们的MiniJavaVM内部类变量的处理方式如下:


而对于类的非静态变量,由于非静态变量是对应于类的实例的,所以处理方法与类变量不同,类的非静态变量在我们的MiniJavaVM中的处理方式如下:


每个类型被装载的时候,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的。如果是用户自定义类装载器装载的,那么虚拟机必须在类型信息中存储对该装载器的引用。这是作为方法表中的类型数据的一部分保存的。

虚拟机会在动态连接期间使用这个信息。当某个类型引用另一个类型的时候,虚拟机会请求装载发起引用类型的类装载器来装载被引用的类型。这个动态连接的过程,对于虚拟机分离命名空间的方式也是至关重要的。 为了能够正确地执行动态连接以及维护多个命名空间,虚拟机需要在方法表中得知每个类都是由哪个类装载器装载的。

对于每一个被装载的类型(不管是类还是接口),虚拟机都会相应地为它创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式把这个实例和存储在方法区中的类型数据关联起来。在我们的MiniJavaVM中java.lang.Class实例与每一个装载类的关系如下,其中虚拟机内部用哈希表来管理已装载类与java.lang.Class类的实例引用的关系:





第五章 内存管理
5.1 对象、堆、方法区的管理
在Java虚拟机中,关于被装载类型的信息存储在一个逻辑上称为方法区的内存中,所有线程共享方法区,因此它们对方法区数据的访问必须被设计为是线程安全的。方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整。同样,方法区也不必是连续的,方法区可以在一个堆(甚至是虚拟机自己的堆)中自由分配。另外,虚拟机也可以允许用户或者程序员指定方法区的初始大小以及最小和最大尺寸等。[4]

Java虚拟机有一条在堆中分配新对象的指令,却没有释放内存的指令。正如无法用Java代码去明确释放一个对象一样,字节码指令也没有对应的功能。虚拟机自己负责如何以及何时释放不再被运行的程序引用的对象所占据的内存。程序本身不用去考虑何时需回收对象所占的内存,通常虚拟机把这个任务交给垃圾收集器。

方法区也可以被垃圾收集。因为虚拟机允许用户定义的类装载器来动态扩展Java程序,因此一些类也会成为程序“不再引用”的类。当某个类变为不再被引用的类时,Java虚拟机可以卸载这个类(垃圾收集),从而使方法区占据的内存保持最小。

为了简单,MiniJavaVM的方法区使用虚拟机自己的堆,不参与垃圾回收,同时,类的静态字段及一些特殊的类的实例(如与每个类相关的Class类的实例)也不参与垃圾回收。

Java对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成。只要有一个对象引用,虚拟机就必须能够快速地定位对象实例的数据。另外,它也必须能通过该对象引用访问相应的类数据(存储于方法区的类型信息)。因此在对象中通常会有一个指向方法区的指针。

一种可能的堆空间设计就是,把堆分成两部分:一个句柄池,一个对象池,而一个对象引用就是一个指向句柄池的本地指针。句柄池的每个条目有两部分:一个指向对象实例变量的指针,一个指向方法区类型数据的指针。这种设计的好处是有利于堆碎片整理,当移动对象池中的对象时,句柄部分只需要一更改一下指针指向的新地址就可以了——就是在句柄池中的那个指针。缺点是每次访问对象的实例变量都要经过再次指针传递。如图5.1.1所示:


另一种设计方式是使对象指针直接指向一组数据而该数据包括对象实例数据及指向方法区中类数据的指针。这个设计的优缺点正好与前面的方法相反,它只需要一个指针就可以访问对象的实例数据,但是移动对象就变得更加复杂。当使用这种堆的虚拟机为了减少内存碎片而移动对象的时候,它必须在整个运行时数据区中更新指向被移动对象的引用。见图5.1.2所示:


有如下几个理由要求虚拟机必须能够通过对象引用得到类(类型)数据:当程序在运行时需要转换某个对象引用为另一种类型时,虚拟机必须要检查这种转换是否被允许,被转换的对象是否的确是被引用的对象或者它的超类型。当程序在执行instanceof操作时,虚拟机也进行了同样的检查。在这两种情况下,虚拟机都需要查看被引用的对象的类型数据。最后,当程序中调用某个实例方法时,虚拟机必须进行动态绑定,换句话说,它不能按照引用的类型来决定将要调用的方法,而必须根据对象的实际类。为此,虚拟机必须再次通过对象的引用去访问类数据。[9]

我们的MiniJavaVM内部通过实例的引用对象能快速查找到类数据是通过哈希表和第一种方法的结合实现的,其具体实现如图5.1.3所示:


这种组织的好处是对象的引用可以直接作为实例数据的起始地址使用,不再使用二重查找,这种组织对于数组结构更有效。同时,用哈希表来解决根据一个对象的引用找到指向类数据的指针的的问题,最大限度地解决了查找的时间问题。这种表示方法带来的好处在java数组表示中更明显。在我们的MiniJavaVM内部Java数组类型表示如图5.1.4所示:


这种设计同样用来完成我们的MiniJavaVM对象引用与类方法表的对应关系,如图5.1.5所示:


5.2 MiniJavaVM的垃圾回收过程
当虚拟机在运行过程中堆已没有可用分配空间时,虚拟机会启动垃圾收集来回收可用空间。当一个对象不再被程序所引用时,它所使用的堆空间可以被回收,以便被后续的新对象所使用。垃圾收集器必须能判断哪些对象是不再被引用的,并且能够把它们所占据的堆空间释放出来。在释放不再被引用的对象的过程中,垃圾收集器运行将要被释放的对象的终结方法(finalize)。

我们的MiniJavaVM的垃圾收集主要通过JavaVM工程的JavaVM类的如下接口实现:


MiniJavaVM运行垃圾收集的流程如图5.2.1所示:







第六章 执行引擎——Java操作码实现
6.1 Java虚拟机中的操作码功能分类
执行引擎的抽象规范是根据指令集来定义的,Java中的操作码分为栈和局部变量操作指令,类型转换指令,整数运算指令,逻辑运算指令,浮点运算指令,对象和数组指令,控制流指令,异常处理指令,finally子句指令,方法调用与返回指令,线程同步指令。[10]

Ø 栈和局部变量操作指令

ü 将常量池入指令

aconst_null, iconst_m1, iconst_0, iconst_1, iconst_2, iconst_3, iconst_4, iconst_5, lconst_0, lconst_1, fconst_0, fconst_1, fconst_2, dconst_0, dconst_1, bipush, sipush, ldc, ldc_w, ldc_w

ü 从栈中的局部变量中装载值指令

iload, lload, fload, dload, aload, iload_0, iload_1, iload_2, iload_3, lload_0, lload_1, lload_2, lload_3, fload_0, fload_1, fload_2, fload_3, dload_0, dload_1, dload_2, dload_3, aload_0, aload_1, aload_2, aload_3, iaload, laload, faload, daload, aaload, baload, caload, saload

ü 将栈中的值存入局部变量指令

istore, lstore, fstore, astore, istore_0, istore_1, istore_2, istore_3, lstore_0, lstore_1, lstore_2, lstore_3, fstore_0, fstore_1, fstore_2, fstore_3, dstore_0, dstore_1, dstore_2, dstore_3, astore_0, astore_1, astore_2, astore_3, iastore, lastore, fastore, dastore, aastore, bastore, castore, sastore

ü 通用栈操作指令

nop, pop, pop2, dup, dup_x1, dup2, dup2_x1, dup2_x2, swap

Ø 类型转换指令

i2l, i2f, i2d, l2i, l2f, l2d, f2i, f2l, f2d, d2i, d2l, d2f, i2b, i2c, i2s

Ø 整数运算指令

iadd, ladd, isub, lsub, imul, lmul, idiv, ldiv, irem, lrem, ineg, lneg, iinc

Ø 逻辑运算指令

ü 移位操作指令

ishl, lshl, ishr, lshr, iushr, lushr,

ü 按位布尔运算指令

iand, land, ior, lor, ixor, lxor

Ø 浮点运算指令

fadd, dadd, fsub, dsub, fmul, dmul, fdiv, ddiv, frem, drem, fneg, dneg

Ø 对象和数组指令

ü 对象操作指令

new, checkcast, getfield, putfield, getstatic, putstatic, instanceof

ü 数组操作指令

newarray, anewarray, arraylength, multinewarray

Ø 控制流指令

ü 条件分支指令

ifeq, ifne, iflt, ifge, ifgt, if_icmpeq, if_icmpne, if_icmplt, if_icmpge, if_icmpgt, if_icmple, ifnull, ifnonnull, if_acmpeq, if_acmpne

ü 比较指令

lcmp, fcmpl, fcmpg, dcmpl, dcmpg

ü 无条件转移指令

goto, goto_w

ü 表跳转指令

tableswitch, lookupswitch

Ø 异常指令

athrow

Ø finally子句指令

jsr, jsr_w, ret

Ø 方法调用与返回指令

ü 方法调用指令

invokevirtual, invokespecial, invokestatic, invokeinterface

ü 方法返回指令

ireturn, lreturn, freturn, dreturn, areturn, return

Ø 线程同步指令

monitorenter, monitorexit

6.2 操作码功能实现——JavaOperatorExecute类
我们的JavaVM工程中的JavaOperatorExecute类负责解释Java虚拟机中200多个操作码的功能。Java虚拟机的每一个或每几个类似功能的操作码都对应于JavaOperatorExecute类的一个方法,如bipush指令,对应于JavaOperatorExecute类中的function_bipush()方法,而iload, lload, dload, fload, aload指令,对应于JavaOperatorExecute类中的function_xload()方法。这些方法主要实现两个功能:一、按照操作码的功能说明实现这个操作码的功能,二、执行完这个操作码的功能后修改当前PC的值。在执行操作码的过程中,如果有抛出异常,则设置抛出异常的标志,JavaOperatorExecute类根据这个标志处理异常。

对于一个非本地方法的操作码序列,在JavaOperatorExecute类的构造函数中得到这个方法的操作码序列和操作码长度,再通过一个大的switch-case结构找到这个操作码应该对应的JavaOperatorExecute中的方法,执行完这个方法后,如果还存在未执行的操作码或还没有抛出异常,则继续执行,否则该方法执行完毕。JavaOperatorExecute类的执行过程如下图所示:





第七章 方法调用的实现
7.1 Java中的方法调用
Java程序设计语言提供了两种基本的方法:实例方法和类(或静态)方法。这两种方法的区别在于:[6]

1) 实例方法在被调用之前,需要一个实例,而类方法不需要

2) 实例方法使用动态(迟)绑定,而类方法使用静态(早)绑定

当Java虚拟机调用一个类方法时,它会基于对象引用的类型(通常在编译时可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会基于对象实际的类(只能在运行时得知)来选择所调用的方法。

Java虚拟机使用两种不同的指令分别调用这两种方法。对于实例方法,使用invokevirtual指令,对于类方法,使用invokestatic指令。这两种指令如表7.1.1所示:

表7.1.1方法调用

操作码
操作数
说明

Invokevirtual
Indexbyte1, indexbyte2
把objectref(对象引用)和args(参数)从栈中弹出,调用常量池索引指向的实例方法

Invokestatic
Indexbyte1, indexbyte2
把args从栈中弹出,调用常量池索引指向的类方法


尽管通常使用invokevirtual指令调用实例方法,但在某些特定的情况中,也会使用另外两种操作码——invokespecial和invokeinterface,如表7.1.2所示

表7.1.2 方法调用

操作码
操作数
说明

Invokespecial
Indexbyte1, indexbyte2
把objectref和args从栈中弹出,调用常量池索引指向的实例方法

Invokeinterface
Indexbyte1, indexbyte2
把objectref和args从栈中弹出,调用常量池索引指向的实例方法


当根据引用的类型来调用实例方法,而不是根据对象的类来调用的时候,通常使用invokespecial指令。这又分为三种情况:

1)实例初始化(<init>())方法

2)私有方法

3)使用super关键字所调用的方法

当给出一个接口的引用时,使用invokeinterface来调用一个实例方法。

Java虚拟机总是直接调用类初始化(<clinit>())方法,类的初始化方法永远不会被任何字节码调用。在Java虚拟机指令集中,没有任何调用<clinit>()方法的指令,如果class文件尝试使用指令来调用<clinit>()方法,会导致虚拟机抛出异常。

到方法的引用最初是符号化的。所有的调用指令(例如invokevirtual和invokestatic)都指向一个最初包含符号引用的常量池入口,当Java虚拟机遇到一条调用指令时,如果还没有解析符号引用,那么虚拟机把解析符号引用作为执行指令调用执行过程中的一部分。要解析一个符号引用,Java虚拟机要确定被符号化引用的方法,然后再用一个直接引用来代替符号引用。直接引用就如同偏移量指针一样,如果将来再次使用该引用,它可以使虚拟机更快地调用这个方法。

一旦解析了一个方法后,Java虚拟机就准备调用它。如果这个方法是一个实例方法,它必须在一个对象中被调用。对每一次实例方法的调用,虚拟机需要在栈里存在一个对象引用(objectref)。如果该方法需要参数,那么除了objectref,虚拟机还需要在栈中存在该方法所需要的参数(args)。如果这个方法是一个类方法,虚拟机不再需要objrectref,因为虚拟机不会在对象上调用一个类方法,栈中存在的将只有args。Objectref和args(或者在类方法的情况下只能args)必须在调用指令执行前,被其他指令压入所调用方法的操作数栈。

7.2 非本地方法的实现
JavaVM工程中非本地方法的实现主要是通过JavaOperatorExecute类来完成的。以invokevirtual指令为例,当虚拟机执行到调用方法的操作码时,先根据操作码计算的索引来查找CONSTANT_Methodref_info入口。如果该入口尚未存在,那么虚拟机将会解析这个常量池入口,在这个解析过程中产生方法的方法索引表index和该方法的参数数目args。然后虚拟机新建一个java栈帧,将当前Java栈帧中的栈里的变量出栈,赋给新的栈帧的局部变量,虚拟机把objectref赋给位置为0的局部变量,arg1赋给位置为1的局部变量,等等。然后新栈帧入栈,虚拟机把程序计数器(PC)设为新方法第一条指令的地址,然后在新位置继续执行。当新的非本地方法调用结束后,该栈帧出栈,如果有返回值,该方法的返回值压入调用该方法的java栈帧的栈中。调用该方法的Java栈帧重新成为当前栈帧。非本地方法的调用过程实现如图7.2.1所示:


7.3 本地方法的实现
7.3.1 Java中的本地方法
Java的本地方法接口,或者称作JNI,是为可移植性准备的。JNI设计的可以被任何Java虚拟机实现支持,而不管它们使用何种垃圾收集或者对象表示技术。这样它能使开发者在一个特定的主机平台上,把同样的(与JNI兼容的)本地方法二进制形式连接到任何支持JNI的虚拟机实现上。实现设计者可以选择创建一些私有的本地方法接口,扩展或者取代JNI。[14]

虚拟机使用一种“与实现相关”的风格调用本地方法。当调用本地方法时,虚拟机不会将一个新的栈帧压入Java栈。当线程进入到本地方法的那一刻,它就将Java栈抛在身后,直到本地方法返回后,Java栈才被重新使用。[6]

一个具有平台无关性的Java程序以以下方式组织:




一个具有平台无关性的Java程序

而一个具有平台机关性的Java程序以以下方式组织:


一个平台相关的Java程序

因此,为了实现本地方法接口(JNI),就必须与主机操作系统打交道。这里,为了实现Java虚拟机的功能,设计为Java虚拟机在调用本地方法时,使用重新写过的本地方法库(在windows下为dll),通过给定的函数名及一些信息,装载正确的动态链接库,并找到正确的方法执行。对于已经装载的动态链接库,JavaVM都将保留其句柄,放在一个链表中,以减少额外的开销。

MiniJavaVM对本地方法的实现使用调用动态链接库中的方法来实现。例如对于Java中的本地方法java.lang.System类的registerNatives()方法,对应的本地方法是:

java_void _MiniJava_java_lang_System_registerNatives(JavaVM* pJVM,java_class clsref);

该方法对应的动态链接库正是由JavaNativeCall工程生成的。具体调用时,只要在装载该方法所在的动态链接库后再得到该方法的地址,将参数压栈,调用这个方法,就能完成这个方法的调用,也就实现了Java本地方法。

7.3.2 NativeMethod_access类
NativeMethod_access类是JavaVM工程中负责本地方法调用的类,这个类所作的工作有:

² 如果动态链接库还未装载,则装载该动态链接库

² 根据调用的方法名和描述符及方法所在的类名生成本地方法名

² 找到动态链接库中该本地方法的位置

² 将调用参数以汇编的方法入栈

² 调用该方法

以java.lang.System类的registerNatives()方法为例,首先,NativeMethod_access类的实例确保已经装载了本地方法所在的动态链接库MiniJava.dll,然后根据method_info实例的信息生成调用的本地方法的方法名:_MiniJava_java_lang_System_registerNatives。用GetProcAddress()取得该本地方法的地址,然后调用参数pJVM和clsref入栈,最后调用该方法。

7.3.3 如何处理本地方法可变参数问题
由于本地方法的调用不是由Java栈帧和操作码完成的,这里必然涉及参数如何入栈的问题,对于实现我们的MiniJavaVM的语言c++来说,没有在语言级可以动态确定调用参数的用法,而对于Java虚拟机的本地方法实现,要压栈的数据类型归根到底也只有word(unsigned long),java_float(float),java_double(double),java_long(__int64)这四种类型,所以用汇编来实现参数压栈应该是最简单的。

为此,特定义如下宏实现特定参数类型的入栈功能:


宏PUSH_METHOD_PARAM实现了本地方法入栈的功能(注意,该宏不能改写为函数,这与寄存器ESP的值相关):


有了上述宏后,将需要的参数压栈,就完美解决了我们的MiniJavaVM虚拟机本地方法调用的问题。





第八章 异常的实现
8.1 Java中的异常
有了异常处理,Java程序就能在程序运行时平稳地处理意外情况。Java虚拟机主要通过异常表来处理异常。异常表在Class文件中的结构如下:

表8.1.1 Exception_info表的格式

类型
名称
数量

U2
Start_pc
1

U2
End_pc
1

U2
Handler_pc
1

U2
Catch_type
1


其中start_pc给出从代码数组起始处到异常处理起始处的偏移量;end_pc项给出从代码数组到异常处理结束后一个字节的偏移量;handler_pc项给出一条指令从代码数组起始处跳转到异常处理器的第1条指令的偏移量——如果抛出的异常被该项捕获的话;catch_type项给出被该异常处理器所捕获的异常
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值