这个虚拟机实际上相当简单。它使用堆栈体系结构,这意味着在使用指令操作数之前要先将它们装入内部堆栈。指令集包含所有的常规算术和逻辑运算,以及条件转移和无条件转移、装入/存储、调用/返回、堆栈操作和几种特殊类型的指令。有些指令包含立即操作数值,它们被直接编码到指令中。其它指令直接引用常量池中的值。
诸如 C 和 C++ 这些编译成本机代码的语言通常在编译完源代码之后需要链接这个步骤。这一链接过程将来自独立编译好的各个源文件的代码和共享库代码合并起来,从而形成了一个可执行程序。Java 语言就不同。使用 Java 语言,由编译器生成的类在被装入到 JVM 之前通常保持原状。即使从类文件构建 JAR 文件也不会改变这一点 ― JAR 只是类文件的容器。
链接类不是一个独立步骤,它是在 JVM 将这些类装入到内存时所执行作业的一部分。在最初装入类时这一步会增加一些开销,但也为 Java 应用程序提供了高度灵活性。例如,在编写应用程序以使用接口时,可以到运行时才指定其实际实现。这个用于组装应用程序的?后联编方法广泛用于 Java 平台,servlet 就是一个常见示例。
JVM 规范中详细描述了装入类的规则。其基本原则是只在需要时才装入类(或者至少看上去是这样装入 ― JVM 在实际装入时有一些灵活性,但必须保持固定的类初始化顺序)。每个装入的类都可能拥有其它所依赖的类,所以装入过程是递归的。清单 2 中的类显示了这一递归装入的工作方式。?Demo
?类包含一个简单的?main
?方法,它创建了?Greeter
?的实例,并调用?greet
?方法。?Greeter
?构造函数创建了?Message
?的实例,随后会在?greet
?方法调用中使用它。
public void greet() {
s_message.print(System.out);
}
}
public class Message
{
private String m_text;
public Message(String text) {
m_text = text;
}
public void print(java.io.PrintStream ps) {
ps.println(m_text);
}
}
在?java
?命令行上设置参数?-verbose:class
?会打印类装入过程的跟踪记录。清单 3 显示了使用这一参数运行清单 2 程序的部分输出:
这只列出了输出中最重要的部分 ― 完整的跟踪记录由 294 行组成,我删除了其中大部分,形成了这个清单。最初的一组类装入(本例中是 279 个)都是在尝试装入?Demo
?类时触发的。这些类是每个 Java 程序(不管有多小)都要使用的核心类。即使删除?Demo main
?方法的所有代码也不会影响这个初始的装入顺序。但是不同版本的类库所涉及的类数量和名称都不同。
在上面这个清单中,装入?Demo
?类之后的部分更有趣。这里的顺序显示了只有在准备创建?Greeter
?类的实例时才会装入该类。不过,?Greeter
类使用了?Message
?类的静态实例,所以在可以创建?Greeter
?类的实例之前,还必须先装入?Message
?类。
在装入并初始化类时,JVM 内部会完成许多操作,包括解码二进制类格式、检查与其它类的兼容性、验证字节码操作的顺序以及最终构造java.lang.Class
?实例来表示新类。这个?Class
?对象成了 JVM 创建新类的所有实例的基础。它还是已装入类本身的标识 ― 对于装入到 JVM 的同一个二进制类,可以有多个副本,每个副本都有其自己的?Class
?实例。即使这些副本都共享同一个类名,但对 JVM 而言它们都是独立的类。
装入到 JVM 的类是由?类装入器控制的。JVM 中构建了一个?引导程序类装入器,它负责装入基本的 Java 类库类。这个特殊的类装入器有一些专门的特性。首先,它只装入在引导类路径上找到的类。因为这些是可信的系统类,所以引导程序装入器跳过了对常规(不可信)类所做的大量验证。
引导程序不是唯一的类装入器。对于初学者而言,JVM 为装入标准 Java 扩展 API 中的类定义了一个?扩展类装入器,并为装入一般类路径上的类(包括应用程序类)定义了一个?系统类装入器。应用程序还可以定义它们自己的用于特殊用途(例如运行时类的重新装入)的类装入器。这样添加的类装入器派生自?java.lang.ClassLoader
?类(可能是间接派生的),该类对从字节数组构建内部类表示(?java.lang.Class
?实例)提供了核心支持。每个构造好的类在某种意义上是由装入它的类装入器所“拥有”。类装入器通常保留它们所装入类的映射,从而当再次请求某个类时,能通过名称找到该类。
每个类装入器还保留对父类装入器的引用,这样就定义了类装入器树,树根为引导程序装入器。在需要某个特定类的实例(由名称来标识)时,无论哪个类装入器最初处理该请求,在尝试直接装入该类之前,一般都会先检查其父类装入器。如果存在多层类装入器,那么会递归执行这一步,所以这意味着通常不仅在装入该类的类装入器中该类是?可见的,而且对于所有后代类装入器也都是可见的。这还意味着如果一条链上有多个类装入器可以装入某个类,那么该树最上端的那个类装入器会是实际装入该类的类装入器。
在许多环境中,Java 程序会使用多个应用程序类装入器。J2EE 框架就是一个示例。该框架装入的每个 J2EE 应用程序都需要拥有一个独立的类装入器以防止一个应用程序中的类干扰其它应用程序。该框架代码本身也将使用一个或多个其它类装入器,同样用来防止对应用程序产生的或来自应用程序的干扰。整个类装入器集合形成了树状结构的层次结构,在其每个层次上都可装入不同类型的类。
作为类装入器层次结构的实际示例,图 1 显示了 Tomcat servlet 引擎定义的类装入器层次结构。这里 Common 类装入器从 Tomcat 安装的某个特定目录的 JAR 文件进行装入,旨在用于在服务器和所有 Web 应用程序之间共享代码。Catalina 装入器用于装入 Tomcat 自己的类,而 Shared 装入器用于装入 Web 应用程序之间共享的类。最后,每个 Web 应用程序有自己的装入器用于其私有类。

在这种环境中,跟踪合适的装入器以用于请求新类会很混乱。为此,在 Java 2 平台中将?setContextClassLoader
?方法和getContextClassLoader
?方法添加到了?java.lang.Thread
?类中。这些方法允许该框架设置类装入器,使得在运行每个应用程序中的代码时可以将类装入器用于该应用程序。
能装入独立的类集合这一灵活性是 Java 平台的一个重要特性。尽管这个特性很有用,但是它在某些情况中会产生混淆。一个令人混淆的方面是处理 JVM 类路径这样的老问题。例如,在图 1 显示的 Tomcat 类装入器层次结构中,由 Common 类装入器装入的类决不能(根据名称)直接访问由 Web 应用程序装入的类。使这些类联系在一起的唯一方法是通过使用这两个类集都可见的接口。在这个例子中,就是包含由 Java servlet 实现的?javax.servlet.Servlet
?。
无论何种原因在类装入器之间移动代码时都会出现问题。例如,当 J2SE 1.4 将用于 XML 处理的 JAXP API 移到标准分发版中时,在许多环境中都产生了问题,因为这些环境中的应用程序以前是依赖于装入它们自己选择的 XML API 实现的。使用 J2SE 1.3,只要在用户类路径中包含合适的 JAR 文件就可以解决该问题。在 J2SE 1.4 中,这些 API 的标准版现在位于扩展的类路径中,所以它们通常将覆盖用户类路径中出现的任何实现。
使用多个类装入器还可能引起其它类型的混淆。图 2 显示了?类身份危机(class identity crisis)的示例,它是在两个独立类装入器都装入一个接口及其相关的实现时产生的危机。即使接口和类的名称和二进制实现都相同,但是来自一个装入器的类的实例不能被认为是实现了来自另一个装入器的接口。图 2 中通过将接口类?I
?移至 System 类装入器的空间就可以解除这种混淆。类?A
?仍然有两个独立的实例,但它们都实现了同一个接口?I
?。

Java 类定义和 JVM 规范一起为运行时组装代码定义了功能极其强大的框架。通过使用类装入器,Java 应用程序能使用多个版本的类,否则这些类就会引起冲突。类装入器的灵活性甚至允许动态地重新装入已修改的代码,同时应用程序继续执行。
这里,Java 平台灵活性在某种程度上是以启动应用程序时较高的开销作为代价的。在 JVM 可以开始执行甚至最简单的应用程序代码之前,它都必须装入数百个独立的类。相对于频繁使用的小程序,这个启动成本通常使 Java 平台更适合于长时间运行的服务器类型的应用程序。服务器应用程序还最大程度地受益于代码在运行时进行组装这种灵活性,所以对于这种开发,Java 平台正日益受宠也就不足为奇了。
在本系列文章的第 2 部分中,我将介绍使用 Java 平台动态基础的另一个方面:反射 API(Reflection API)。反射使执行代码能够访问内部类信息。这可能是构建灵活代码的极佳工具,可以不使用类之间任何源代码链接就能够在运行时将代码挂接在一起。但象使用大多数工具一样,您必须知道何时及如何使用它以获得最大利益。请阅读?Java 编程的动态性第 2 部分以了解有效反射的诀窍和利弊
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!