CSDN博客

img sarsor

Java 编程的动态性,第 1 部分:类和类装入

发表于2004/7/6 17:35:00  1067人阅读

分类: Open Source Java

(有删减)

研究类以及 JVM 装入类时所发生的情况
级别:中级

Dennis M. Sosnoskidms@sosnoski.com
总裁,Sosnoski Software Solutions, Inc.
2003 年 6 月

装入类
诸如 C 和 C++ 这些编译成本机代码的语言通常在编译完源代码之后需要链接这个步骤。这一链接过程将来自独立编译好的各个源文件的代码和共享库代码合并起来,从而形成了一个可执行程序。Java 语言就不同。使用 Java 语言,由编译器生成的类在被装入到 JVM 之前通常保持原状。即使从类文件构建 JAR 文件也不会改变这一点 — JAR 只是类文件的容器。

链接类不是一个独立步骤,它是在 JVM 将这些类装入到内存时所执行作业的一部分。在最初装入类时这一步会增加一些开销,但也为 Java 应用程序提供了高度灵活性。例如,在编写应用程序以使用接口时,可以到运行时才指定其实际实现。这个用于组装应用程序的后联编方法广泛用于 Java 平台,servlet 就是一个常见示例。

JVM 规范中详细描述了装入类的规则。其基本原则是只在需要时才装入类(或者至少看上去是这样装入 — JVM 在实际装入时有一些灵活性,但必须保持固定的类初始化顺序)。每个装入的类都可能拥有其它所依赖的类,所以装入过程是递归的。清单 2 中的类显示了这一递归装入的工作方式。Demo 类包含一个简单的 main 方法,它创建了 Greeter 的实例,并调用 greet 方法。Greeter 构造函数创建了 Message 的实例,随后会在 greet 方法调用中使用它。

清单 2. 类装入演示的源代码
public class Demo
{
    public static void main(String[] args) {
        System.out.println("**beginning execution**");
        Greeter greeter = new Greeter();
        System.out.println("**created Greeter**");
        greeter.greet();
    }
}

public class Greeter
{
    private static Message s_message = new Message("Hello, World!");
   
    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 程序的部分输出:

清单 3. -verbose:class 的部分输出

[Opened /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/sunrsasign.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/jsse.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/jce.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/charsets.jar]
[Loaded java.lang.Object from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.io.Serializable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.CharSequence from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.String from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
...
[Loaded java.security.Principal from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.security.cert.Certificate
  from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded Demo]
**beginning execution**
[Loaded Greeter]
[Loaded Message]
**created Greeter**
Hello, World!
[Loaded java.util.HashMap$KeySet
  from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.util.HashMap$KeyIterator
  from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]

这只列出了输出中最重要的部分 — 完整的跟踪记录由 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 应用程序有自己的装入器用于其私有类。

图 1. Tomcat 类装入器
Tomcat 类装入器

在这种环境中,跟踪合适的装入器以用于请求新类会很混乱。为此,在 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

图 2. 类身份危机
类身份危机

结束语
Java 类定义和 JVM 规范一起为运行时组装代码定义了功能极其强大的框架。通过使用类装入器,Java 应用程序能使用多个版本的类,否则这些类就会引起冲突。类装入器的灵活性甚至允许动态地重新装入已修改的代码,同时应用程序继续执行。

这里,Java 平台灵活性在某种程度上是以启动应用程序时较高的开销作为代价的。在 JVM 可以开始执行甚至最简单的应用程序代码之前,它都必须装入数百个独立的类。相对于频繁使用的小程序,这个启动成本通常使 Java 平台更适合于长时间运行的服务器类型的应用程序。服务器应用程序还最大程度地受益于代码在运行时进行组装这种灵活性,所以对于这种开发,Java 平台正日益受宠也就不足为奇了。

在本系列文章的第 2 部分中,我将介绍使用 Java 平台动态基础的另一个方面:反射 API(Reflection API)。反射使执行代码能够访问内部类信息。这可能是构建灵活代码的极佳工具,可以不使用类之间任何源代码链接就能够在运行时将代码挂接在一起。但象使用大多数工具一样,您必须知道何时及如何使用它以获得最大利益。请阅读 Java 编程的动态性第 2 部分以了解有效反射的诀窍和利弊。

参考资料

关于作者
Dennis Sosnoski 的照片 Dennis Sosnoski 是西雅图地区 Java 咨询公司 Sosnoski Software Solutions, Inc. 的创始人和首席顾问,他是 J2EE、XML 和 Web 服务支持方面的专家。他已经有 30 多年专业软件开发经验,最近几年他集中研究服务器端的 Java 技术。Dennis 经常在全国性的会议上就 XML 和 Java 技术发表演讲,他还是 Seattle Java-XML SIG 的主席。可以通过 dms@sosnoski.com 与 Dennis 联系。

0 0

相关博文

我的热门文章

img
取 消
img