类装入问题解密,第 1 部分: 类装入和调试工具介绍

来源:互联网 发布:淘宝店如何升级 编辑:程序博客网 时间:2024/06/07 04:47

类装入问题解密,第 1 部分: 类装入和调试工具介绍

学习类装入的工作方式以及 JVM 如何帮助找出类装入问题

developerWorks 文档选项 将此页作为电子邮件发送

将此页作为电子邮件发送


对此页的评价

帮助我们改进这些内容


级别: 中级

Lakshmi Shankar , Java 技术中心开发团队, IBM Hursley 实验室
Simon Burns , Java 技术中心开发团队, IBM Hursley 实验室

2005 年 12 月 29 日

类装入组件是 Java™ 虚拟机的基础。虽然开发人员一般对类装入的基础有良好的掌握,但是当问题发生时,在诊断问题和确定解决方案方面可能还要有一定的困难。在这份由四部分组成的系列中,Lakshmi Shankar 和 Simon Burns 讨论了在 Java 开发中可能遇到的各种类装入问题,解释了它们为什么会发生和如何解决它们。他们提供的见解有助于理解和解决常见的 Java 异常,例如 NoClassDefFoundErrorClassNotFoundException,以及更有挑战性的问题,例如类装入器约束违反和死锁。在第 1 部分中,他们详细描述了 Java 类装入的工作方式,讨论了 JVM 中可以帮助诊断类装入问题的工具。

类装入器负责把类装入 Java 虚拟机(JVM)。简单的应用程序可以用 Java 平台内置的类装入工具装入类;更复杂的应用程序则倾向于定义自己定制的类装入器。但是,不论使用哪种类装入器,在类装入过程中都可能发生许多问题。如果想避免这类问题,需要理解类装入的基本机制。当问题发生时,对于可用的诊断特性和调试技术的了解会有助于解决问题。

在这个系列的文章中,我们将深入研究类装入的问题,并用丰富的示例演示它们。这份介绍性的文章的第一节描述类装入的基础;第二节介绍一些 JVM 调试特性。系列中剩下的三篇文章将侧重于解决类装入异常,并演示一些可能会碰到的更复杂的类装入问题。

类装入基础

这一节描述类装入的核心概念,为系列剩下的部分提供知识基础。

类装入器委托

类装入器委托模型 是把装入请求相互传给对方的类装入器图。引导 类装入器是这个图的根。用单一委托父类 创建类装入器,并在以下位置寻找类:

  • 缓存(Cache)
  • 父类(Parent)
  • 自己(Self)

类装入器首先判断要求它装入的类是否与过去装入的类相同。如果相同,就返回上次返回的类(即保存在缓存中的类)。如果不是,就把装入类的机会交给父类。这两步递归地以深度优先的方式重复。如果父类返回 null(或抛出 ClassNotFoundException),那么类装入器会在自己的路径中寻找类的源。

因为父类类装入器总是先得到装入类的机会,所以类装入器装入的类最靠近根。这意味着所有核心引导类都是由引导装入器装入的,这就保证装入了类(例如 java.lang.Object)的正确版本。这也可以让类装入器看到自己或父类或祖先装入的类,但是不能看到子女装入的类。

图 1 显示了三个标准的类装入器:


图 1. 类装入器委托模型
类装入器委托模型

与其他类装入器不同,引导类装入器(也称作基本(primordial) 类装入器)不能由 Java 代码实例化。(通常是因为它是作为 VM 本身的一部分实现的。)这个类装入器可以从启动的类路径装入核心系统类,通常是位于 jre/lib 目录的 JAR 文件。但是不能用 -Xbootclasspath 命令行选项修改这个类路径(稍后介绍)。

扩展(extension) 类装入器(也称作标准扩展 类装入器)是引导类装入器的一个孩子。它的主要职责是从扩展目录装入类,通常位于 jre/lib/ext 目录。这提供了简单地访问新扩展的能力,例如不同的安全扩展,不需要修改用户的类路径即可实现。

系统(system) 类装入器(也称作应用程序 类装入器)负责从 CLASSPATH 环境变量指定的路径装入代码。默认情况下,这个类装入器是用户创建的任何类装入器的父类。这也是 ClassLoader.getSystemClassLoader() 方法返回的类装入器。

类路径选项

表 1 总结了设置三个标准类装入器的类路径的命令行选项:

表 1. 类路径选项 命令行选项 解释 涉及的类装入器 -Xbootclasspath:<用 ; 或 : 分隔的目录和 zip/JAR 文件> 设置引导类和资源的搜索路径。 引导 -Xbootclasspath/a:<用 ; 或 : 分隔的目录和 zip/JAR 文件> 把路径添加到启动类路径的末尾。 引导 -Xbootclasspath/p:<用 ; 或 : 分隔的目录和 zip/JAR 文件> 把路径添加到启动类路径的前面。 引导 -Dibm.jvm.bootclasspath=<用 ; 或 : 分隔的目录和 zip/JAR 文件> 这个属性的值被用作额外的搜索路径,它被插到 -Xbootclasspath/p: 定义的值和启动类路径之间。启动类路径或者是默认值,或者是 -Xbootclasspath: 选项定义的值。 引导 -Djava.ext.dirs=<用 ; 或 : 分隔的目录和 zip/JAR 文件> 指定扩展类和资源的搜索路径。 扩展 -cp or -classpath <用 ; 或 : 分隔的目录和 zip/JAR 文件> 设置应用程序类和资源的搜索路径。 系统 -Djava.class.path=<用 ; 或 : 分隔的目录和 zip/JAR 文件> 设置应用程序类和资源的搜索路径。 系统

类装入的阶段

类的装入实际上可以分成三个阶段:装入、链接和初始化。

虽然不是所有的问题,但至少大多数与类装入有关的问题都可以追溯到在这三个阶段中发生的某个问题。所以,对于每一阶段的深入理解有助于对类装入问题的诊断。图 2 显示了这三个阶段:


图 2. 类装入的阶段
类装入的阶段

装入 阶段包括:找到必要的类(通过查找每个类路径)并装入字节码。在 JVM 中,装入阶段为类对象提供了非常基本的内存结构。在这一阶段不处理方法、字段和引用的其他类。所以,类还不能使用。

链接 是三个阶段中最复杂的一个。可以把它分成三个主要阶段:

  • 字节码验证。 类装入器对于类的字节码要做许多检测,以确保格式正确、行为正确。
  • 类准备。 这个阶段准备代表每个类中定义的字段、方法和实现接口所必需的数据结构。
  • 解析。 在这个阶段,类装入器装入类所引用的其他所有类。可以用许多方式引用类:
    • 超类
    • 接口
    • 字段
    • 方法签名
    • 方法中使用的本地变量

初始化 阶段,类中包含的静态初始化器都被执行。在这一阶段末尾,静态字段被初始化成默认值。

在这三个阶段末尾,类被完整地装入,可以使用了。请注意可以用惰性方式执行类装入,所以类装入过程的某些部分可能在第一次使用类的时候才执行,而不是在装入时执行。

显式装入与隐式装入

类装入的方式有两种 —— 显式隐式,两者之间有些细微差异。

显式 类装入发生在使用以下方法调用装入的类的时候:

  • cl.loadClass()cljava.lang.ClassLoader 的实例)
  • Class.forName()(启动的类装入器是当前类定义的类装入器)

当调用其中一个方法的时候,指定的类(以类名为参数)由类装入器装入。如果类已经装入,那么只是返回一个引用;否则,装入器会通过委托模型装入类。

隐式 类装入发生在由于引用、实例化或继承导致装入类的时候(不是通过显式方法调用)。在每种情况下,装入都是在幕后启动的,JVM 会解析必要的引用并装入类。与显式类装入一样,如果类已经装入了,那么只是返回一个引用;否则,装入器会通过委托模型装入类。

类的装入通常组合了显式和隐式类装入。例如,类装入器可能先显式地装入一个类,然后再隐式地装入它引用的所有类。



回页首

JVM 的调试特性

前面一节介绍了类装入的基本原则。这一节介绍 IBM JVM 中内置的帮助调试的特性。其他 JVM 也有类似的调试特性;请参阅相关文档来了解细节。

详细输出

可以用 -verbose 命令行选项打开 IBM JVM 的详细输出。当某些事件发生的时候(例如,类装入时),详细输出会在控制台上显示信息。要想得到额外的类装入信息,可以用详细类输出。可以用 -verbose:class 选项启动这个模式。

解释详细输出
详细输出列出已经打开的所有 JAR 文件,包括到这些 JAR 的完整路径。下面是一个示例:

...[Opened D:/jre/lib/core.jar in 10 ms][Opened D:/jre/lib/graphics.jar in 10 ms]...

所有装入的类都已经列出,同时还指出它们是从哪个 JAR 文件或目录装入的。例如:

...[Loaded java.lang.NoClassDefFoundError from D:/jre/lib/core.jar][Loaded java.lang.Class from D:/jre/lib/core.jar][Loaded java.lang.Object from D:/jre/lib/core.jar]...

详细类输出显示额外信息,例如在装入超类的时候,在运行静态初始化器的时候。下面是一些示例输出:

...[Loaded HelloWorld from file:/C:/myclasses/][Loading superclass and interfaces of HelloWorld][Loaded HelloWorldInterface from file:/C:/myclasses/][Loading superclass and interfaces of HelloWorldInterface][Preparing HelloWorldInterface][Preparing HelloWorld][Initializing HelloWorld][Running static initializer for HelloWorld]...

详细输出还显示一些内部抛出的异常(如果发生的话),包含堆栈跟踪。

用 -verbose 解决问题
详细输出有助于解决类路径问题,例如没有打开 JAR 文件(因此不在类路径中)或从错误的位置装入了类。

IBM 详细类装入

知道类装入器在哪里寻找类、特定的类是由哪个类装入器装入的,通常很有用。可以用 IBM 详细类装入命令行选项得到这个信息:-Dibm.cl.verbose=<class name>。可以用正则表达式声明类的名称,例如 Hello* 会跟踪所有以 Hello 开头的名称。

这个选项也可用于用户定义的类装入器,只要它们直接或间接地扩展了 java.net.URLClassLoader

解释 IBM 详细类装入的输出
IBM 详细类装入的输出显示了要装入指定类的类装入器以及它们查找的位置。例如,假设用以下命令行:

java -Dibm.cl.verbose=ClassToTrace MainClass

在这里,MainClass 在它的主方法中引用了 ClassToTrace。这会形成像 这里 一样的输出。

在列出类装入器的时候,父类在子女之前列出,因为标准的委托模型的工作方式是父类优先。

请注意,引导类装入器没有输出。只有扩展了 java.net.URLClassLoader 的类装入器才有输出。还请注意,类装入器列在它们的类名之下;如果类装入器有两个实例,那么可能无法区分它们。

用 IBM 详细类装入解决问题
IBM 详细类装入选项是检查所有类装入器设置的类路径的好方法。它还可以指出指定类是由哪个类装入器装入的、从哪里装入的。这样就可以容易地看出是否装入了类的正确版本。

Javadump

Javadump(也称为 Javacore)是另一个很有用的 IBM 诊断工具;要了解它,请参阅 IBM Diagnostics Guides(请参阅 参考资料 中的链接)。当发生以下事件时,JVM 会生成 Javadump:

  • 发生致命的本机异常
  • JVM 用光了堆空间
  • 向 JVM 发送了一个信号(例如,在 Windows 上按下了 Control-Break 或在 Linux 上按下了 Control-/)
  • 调用了 com.ibm.jvm.Dump.JavaDump() 方法

触发 Javadump 的时候,会把详细信息记录到在当前工作目录下保存的一个有日期戳的文本文件中。信息包括线程、锁、堆栈等方面的数据,以及关于系统中类装入器的丰富信息。

解释 Javadump 中的类装入部分
Javadump 文件中提供的确切信息取决于 JVM 在哪个平台上运行。类装入器部分包括:

  • 定义的类装入器和它们之间的关系
  • 每个类装入器装入的类的列表

以下是从 Javadump 提取的类装入器信息的快照:

CL subcomponent dump routine============================ Classpath Z(D:/jre/lib/core.jar),... Oldjava mode false Bootstrapping false  Verbose class dependencies false Class verification VERIFY_REMOTE Namespace to classloader 0x00000000 Start of cache entry pool 0x44D85430 Start of free cache entries 0x44D86204 Location of method table 0x44C23AA0  Global namespace anchor 0x00266894 System classloader shadow 0x00376068 Classloader shadows 0x44D7BA60 Extension loader 0x00ADB830 System classloader 0x00ADB7B0  Classloader summaries         12345678: 1=primordial,2=extension,3=shareable,4=middleware,                   5=system,6=trusted,7=application,8=delegating         -----ta- Loader sun/misc/Launcher$AppClassLoader(0x44D7BA60),                  Shadow 0x00ADB7B0,                  Parent sun/misc/Launcher$ExtClassLoader(0x00ADB830)                 Number of loaded classes 1                 Number of cached classes 260                 Allocation used for loaded classes 1                 Package owner 0x00ADB7B0          -xh-st-- Loader sun/misc/Launcher$ExtClassLoader(0x44D71288),                  Shadow 0x00ADB830,                  Parent *none*(0x00000000)                 Number of loaded classes 0                 Number of cached classes 0                 Allocation used for loaded classes 3                 Package owner 0x00ADB830          p-h-st-- Loader *System*(0x00376068), Shadow 0x00000000                   Number of loaded classes 304                  Number of cached classes 304                          Allocation used for loaded classes 3                      Package owner 0x00000000            ClassLoader loaded classes                           Loader sun/misc/Launcher$AppClassLoader(0x44D7BA60)                   HelloWorld(0x00ACF0E0)         Loader sun/misc/Launcher$ExtClassLoader(0x44D71288)          Loader *System*(0x00376068)                             java/io/WinNTFileSystem(0x002CD118)                         java/lang/Throwable(0x002C03A8)                             java/lang/IndexOutOfBoundsException(0x44D45208)                  java/lang/UnsatisfiedLinkError(0x44D42D38) ....................classes left out to save space........................                 [Ljava/lang/Class;(0x002CA9E8)                   java/io/InputStream(0x002C9818)                   java/lang/Integer$1(0x002C83E8)                           java/util/Dictionary(0x002C4298)

在这个示例中,只有三个标准类装入器:

  • 系统类装入器(sun/misc/Launcher$AppClassLoader
  • 扩展类装入器(sun/misc/Launcher$ExtClassLoader
  • 引导类装入器(*System*

Classloader 汇总部分提供了系统中每个类装入器的细节。在这个系列的文章中,感兴趣的类型是基本、扩展、系统、应用程序委托(用在反射中)。其他类型(共享的、中间件信任的)用在 Persistent Reusable JVM 中,它们超出了这个文章系列的范围(请参阅 Persistent Reusable JVM User Guide 以获得更多信息;在下面的 参考资料 一节中有一个链接)。这个汇总部分还显示了父类类装入器:系统类装入器的父类是 sun/misc/Launcher$ExtClass loader(0x00ADB830)。这个父类地址对应于父类类装入器的原始数据结构(叫作 shadow)。

类装入器装入的类部分列出了每个类装入器装入的类。在这个示例中,系统类装入器只装入了一个类 HelloWorld(在地址 0x00ACF0E0 上)。

用 Javadump 解决问题
使用 Javadump 提供的信息,可以确定系统中存在哪些类装入器。这包括任何用户自定义的类装入器。从装入的类列表中,可以找出特定的类是由哪个类装入器装入的。如果找不到某个类,说明系统中的任何一个类装入器都没有装入它(通常会形成 ClassNotFoundException 异常)。

可以用 Javadump 诊断的其他类型的问题包括:

  • 类装入器命名空间问题。 类装入器的命名空间是类装入器和它装入的所有类的组合。例如,如果某个类存在,但是由错误的类装入器装入(有时会造成 NoClassDefFoundError 异常),那么命名空间就是错误的 —— 也就是说,类在错误的类路径中。为了纠正这种问题,可以试着把类放到不同的位置(例如放在正常的 Java 类路径中),并确保由系统类装入器装入它。

  • 类装入器约束问题。 在这个系列的最后一篇文章中将讨论这种问题的一个示例。

Java 方法跟踪

IBM JVM 有一个内置的方法跟踪工具。这样,不需要修改 Java 代码,就可以跟踪任何 Java 代码(包括核心系统)中的方法。因为这个工具可以提供大量数据,所以可以控制跟踪的级别,只获取需要的信息。

启动跟踪的选项取决于 JVM 的发行版。关于这些选项的细节,请参阅 IBM Diagnostics Guides(请参阅 参考资料 中的链接)。

下面是一些命令行示例:

在 IBM Java 1.4.2 中运行 HelloWorld 时要跟踪所有 java.lang.ClassLoader

java -Dibm.dg.trc.methods=java/lang/ClassLoader.*() -Dibm.dg.trc.print=mt HelloWorld

跟踪 ClassLoader 中的 loadClass() 方法和 HelloWorld 中的方法,也在 IBM Java 1.4.2 中:

java -Dibm.dg.trc.methods=java/lang/ClassLoader.loadClass(),HelloWorld.*()   -Dibm.dg.trc.print=mt HelloWorld

解释方法跟踪的输出
这里 是方法跟踪输出的一个示例(用前面一段的第二个命令行)。

跟踪的每一行都提供了比上面显示的更多的信息。我们来完整地看看上面的一行:

12:57:41.277 0x002A23C0 04000D   > java/lang/ClassLoader.loadClass Bytecode method,        This = 0x00D2B830, Signature: (Ljava/lang/String;Z)Ljava/lang/Class;

这个跟踪包括:

  • 12:57:41.277:方法进入或退出的时间戳。

  • 0x002A23C0:线程 ID。

  • 04000D:某些高级诊断使用的内部 JVM 跟踪点。

  • 余下的信息显示是进入(>)还是退出了(<)方法,后面跟着方法的细节。

用方法跟踪解决问题
可以用方法跟踪解决不同类型的问题,包括:

  • 性能热点: 使用时间戳,可以发现需要花费相当多时间来执行的方法。

  • 挂起: 最后的方法项通常是很好的线索,可以指明应用程序是否挂起。

  • 错误对象: 使用地址,通过与对象的构建函数调用的地址进行比对,可以检查出是不是在正确的对象上调用方法 。

  • 意外的代码路径: 通过跟踪进入和退出点,可以看出程序是否采用了意外的代码路径。

  • 其他错误: 最后的方法项是对错误发生位置的良好提示。


回页首

接下来是什么

在这篇文章中,学习了 JVM 中类装入的基础知识和 IBM JVM 中可以使用的调试特性。在这个系列的下一篇文章中,将学习如何应用这些知识来理解和解决在运行 Java 应用程序时通常会遇到的各种类装入问题。



回页首

参考资料

学习
  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文

  • 了解 Java ClassLoader”(Greg Travis,developerWorks,2001 年 4 月):类装入介绍。

  • Java 编程的动态性,第 1 部分: 类和类装入”(Dennis Sosnoski,developerWorks,2003 年 4 月):理解各种类装入问题,范围从运行简单 Java 应用程序所需要的大量的类,一直到可能在 J2EE 和类似的复杂架构中造成问题的类装入器冲突。

  • JVM 规范:从起源开始全面介绍了 JVM 类文件的格式和指令集。

  • IBM Diagnostics Guides:学习关于调试的更多知识。

  • Persistent Reusable JVM:在线图书,介绍 Persistent Reusable JVM 的概念,并提供编写在它们上面运行的中间件和应用程序的技术指南。

  • Java 技术专区:数百份关于 Java 编程各方面的文章。


获得产品和技术
  • IBM Java developer kits:在 IBM 的一些最流行的平台上创建和运行 J2SE 应用程序。


讨论
  • developerWorks blogs:加入 developerWorks 社区。


回页首

作者简介

Lakshmi Shankar

Lakshmi Shankar 是英国 IBM Hursley 实验室的软件工程师。他为 IBM 工作超过两年,有广泛的经验,一直在 Hursley 实验室从事 Java 性能、测试和开发工作。他目前是 IBM Java 技术的类装入组件的所有人。


Simon Burns

Simon Burns 是 Shiraz(可重置 JVM 和 IBM Java 共享类)组件的所有人,也是 IBM Hursley 实验室的 Java 技术团队负责人。他在 JVM 开发方面工作了三年,专攻 Shiraz 组件和 z/OS 平台。他和 CICS 紧密合作,帮助他们利用 Shiraz 技术。

原创粉丝点击