类装入器负责把类装入 Java 虚拟机(JVM)简单的应用程序可以用 Java 平台内置的类装入工具装入类更复杂的应用程序则倾向于定义自己定制的类装入器但是不论使用哪种类装入器在类装入过程中都可能发生许多问题如果想避免这类问题需要理解类装入的基本机制当问题发生时对于可用的诊断特性和调试技术的了解会有助于解决问题 在这个系列的文章中我们将深入研究类装入的问题并用丰富的示例演示它们这份介绍性的文章的第一节描述类装入的基础第二节介绍一些 JVM 调试特性系列中剩下的三篇文章将侧重于解决类装入异常并演示一些可能会碰到的更复杂的类装入问题 类装入基础 这一节描述类装入的核心概念为系列剩下的部分提供知识基础 类装入器委托 类装入器委托模型 是把装入请求相互传给对方的类装入器图引导 类装入器是这个图的根用单一委托父类 创建类装入器并在以下位置寻找类 缓存(Cache) 父类(Parent) 自己(Self) 类装入器首先判断要求它装入的类是否与过去装入的类相同如果相同就返回上次返回的类(即保存在缓存中的类)如果不是就把装入类的机会交给父类这两步递归地以深度优先的方式重复如果父类返回 null(或抛出 ClassNotFoundException )那么类装入器会在自己的路径中寻找类的源 因为父类类装入器总是先得到装入类的机会所以类装入器装入的类最靠近根这意味着所有核心引导类都是由引导装入器装入的这就保证装入了类(例如 javalangObject )的正确版本这也可以让类装入器看到自己或父类或祖先装入的类但是不能看到子女装入的类 图 显示了三个标准的类装入器 图 类装入器委托模型 与其他类装入器不同引导类装入器(也称作基本(primordial) 类装入器)不能由 Java 代码实例化(通常是因为它是作为 VM 本身的一部分实现的)这个类装入器可以从启动的类路径装入核心系统类通常是位于 jre/lib 目录的 JAR 文件但是不能用 Xbootclasspath 命令行选项修改这个类路径(稍后介绍) 扩展(extension) 类装入器(也称作标准扩展 类装入器)是引导类装入器的一个孩子它的主要职责是从扩展目录装入类通常位于 jre/lib/ext 目录这提供了简单地访问新扩展的能力例如不同的安全扩展不需要修改用户的类路径即可实现 系统(system) 类装入器(也称作应用程序 类装入器)负责从 CLASSPATH 环境变量指定的路径装入代码默认情况下这个类装入器是用户创建的任何类装入器的父类这也是 ClassLoadergetSystemClassLoader() 方法返回的类装入器 类路径选项 表 总结了设置三个标准类装入器的类路径的命令行选项 命令行选项解释涉及的类装入器Xbootclasspath:<用 ; 或 : 分隔的目录和 zip/JAR 文件> 设置引导类和资源的搜索路径引导Xbootclasspath/a:<用 ; 或 : 分隔的目录和 zip/JAR 文件> 把路径添加到启动类路径的末尾 引导Xbootclasspath/p:<用 ; 或 : 分隔的目录和 zip/JAR 文件> 把路径添加到启动类路径的前面引导Dibmjvmbootclasspath=<用 ; 或 : 分隔的目录和 zip/JAR 文件>这个属性的值被用作额外的搜索路径它被插到 Xbootclasspath/p: 定义的值和启动类路径之间启动类路径或者是默认值或者是 Xbootclasspath: 选项定义的值引导Djavaextdirs=<用 ; 或 : 分隔的目录和 zip/JAR 文件> 指定扩展类和资源的搜索路径 扩展 cp or classpath <用 ; 或 : 分隔的目录和 zip/JAR 文件> 设置应用程序类和资源的搜索路径 系统 Djavaclasspath=<用 ; 或 : 分隔的目录和 zip/JAR 文件>设置应用程序类和资源的搜索路径系统 类装入的阶段 类的装入实际上可以分成三个阶段装入链接和初始化 虽然不是所有的问题但至少大多数与类装入有关的问题都可以追溯到在这三个阶段中发生的某个问题所以对于每一阶段的深入理解有助于对类装入问题的诊断图 显示了这三个阶段 图 类装入的阶段 装入 阶段包括找到必要的类(通过查找每个类路径)并装入字节码在 JVM 中装入阶段为类对象提供了非常基本的内存结构在这一阶段不处理方法字段和引用的其他类所以类还不能使用 链接 是三个阶段中最复杂的一个可以把它分成三个主要阶段 字节码验证 类装入器对于类的字节码要做许多检测以确保格式正确行为正确 类准备 这个阶段准备代表每个类中定义的字段方法和实现接口所必需的数据结构 解析 在这个阶段类装入器装入类所引用的其他所有类可以用许多方式引用类 超类 接口 字段 方法签名 方法中使用的本地变量 在初始化 阶段类中包含的静态初始化器都被执行在这一阶段末尾静态字段被初始化成默认值 在这三个阶段末尾类被完整地装入可以使用了请注意可以用惰性方式执行类装入所以类装入过程的某些部分可能在第一次使用类的时候才执行而不是在装入时执行 显式装入与隐式装入 类装入的方式有两种 —— 显式 或 隐式两者之间有些细微差异 显式 类装入发生在使用以下方法调用装入的类的时候 clloadClass()(cl 是 javalangClassLoader 的实例) ClassforName()(启动的类装入器是当前类定义的类装入器) 当调用其中一个方法的时候指定的类(以类名为参数)由类装入器装入如果类已经装入那么只是返回一个引用否则装入器会通过委托模型装入类 隐式 类装入发生在由于引用实例化或继承导致装入类的时候(不是通过显式方法调用)在每种情况下装入都是在幕后启动的JVM 会解析必要的引用并装入类与显式类装入一样如果类已经装入了那么只是返回一个引用否则装入器会通过委托模型装入类 类的装入通常组合了显式和隐式类装入例如类装入器可能先显式地装入一个类然后再隐式地装入它引用的所有类 JVM 的调试特性 前面一节介绍了类装入的基本原则这一节介绍 IBM JVM 中内置的帮助调试的特性其他 JVM 也有类似的调试特性请参阅相关文档来了解细节 详细输出 可以用 verbose 命令行选项打开 IBM JVM 的详细输出当某些事件发生的时候(例如类装入时)详细输出会在控制台上显示信息要想得到额外的类装入信息可以用详细类输出可以用 verbose:class 选项启动这个模式 解释详细输出 详细输出列出已经打开的所有 JAR 文件包括到这些 JAR 的完整路径下面是一个示例
[Opened D:\jre\lib\corejar in ms] [Opened D:\jre\lib\graphicsjar in ms]
所有装入的类都已经列出同时还指出它们是从哪个 JAR 文件或目录装入的例如
[Loaded javalangNoClassDefFoundError from D:\jre\lib\corejar] [Loaded javalangClass from D:\jre\lib\corejar] [Loaded javalangObject from D:\jre\lib\corejar]
详细类输出显示额外信息例如在装入超类的时候在运行静态初始化器的时候下面是一些示例输出
[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 详细类装入命令行选项得到这个信息Dibmclverbose=<class name>可以用正则表达式声明类的名称例如 Hello* 会跟蹤所有以 Hello 开头的名称 这个选项也可用于用户定义的类装入器只要它们直接或间接地扩展了 URLClassLoader 解释 IBM 详细类装入的输出 IBM 详细类装入的输出显示了要装入指定类的类装入器以及它们查找的位置例如假设用以下命令行 java Dibmclverbose=ClassToTrace MainClass 在这里MainClass 在它的主方法中引用了 ClassToTrace 这会形成像 这里 一样的输出 在列出类装入器的时候父类在子女之前列出因为标准的委托模型的工作方式是父类优先 请注意引导类装入器没有输出只有扩展了 URLClassLoader 的类装入器才有输出还请注意类装入器列在它们的类名之下如果类装入器有两个实例那么可能无法区分它们 用 IBM 详细类装入解决问题 IBM 详细类装入选项是检查所有类装入器设置的类路径的好方法它还可以指出指定类是由哪个类装入器装入的从哪里装入的这样就可以容易地看出是否装入了类的正确版本 Javadump Javadump(也称为 Javacore)是另一个很有用的 IBM 诊断工具当发生以下事件时JVM 会生成 Javadump 发生致命的本机异常 JVM 用光了堆空间 向 JVM 发送了一个信号(例如在 Windows 上按下了 ControlBreak 或在 Linux 上按下了 Control\) 调用了 comibmjvmDumpJavaDump() 方法 触发 Javadump 的时候会把详细信息记录到在当前工作目录下保存的一个有日期戳的文本文件中信息包括线程锁堆栈等方面的数据以及关于系统中类装入器的丰富信息 解释 Javadump 中的类装入部分 Javadump 文件中提供的确切信息取决于 JVM 在哪个平台上运行类装入器部分包括 定义的类装入器和它们之间的关系 每个类装入器装入的类的列表 以下是从 Javadump 提取的类装入器信息的快照 CL subcomponent dump routine ============================ Classpath Z(D:\jre\lib\corejar) Oldjava mode false Bootstrapping false Verbose class dependencies false Class verification VERIFY_REMOTE Namespace to classloader x Start of cache entry pool xD Start of free cache entries xD Location of method table xCAA Global namespace anchor x System classloader shadow x Classloader shadows xDBA Extension loader xADB System classloader xADBB Classloader summaries : =primordial=extension=shareable=middleware =system=trusted=application=delegating ta Loader sun/misc/Launcher$AppClassLoader(xDBA) Shadow xADBB Parent sun/misc/Launcher$ExtClassLoader(xADB) Number of loaded classes Number of cached classes Allocation used for loaded classes Package owner xADBB xhst Loader sun/misc/Launcher$ExtClassLoader(xD) Shadow xADB Parent *none*(x) Number of loaded classes Number of cached classes Allocation used for loaded classes Package owner xADB phst Loader *System*(x) Shadow x Number of loaded classes Number of cached classes Allocation used for loaded classes Package owner x ClassLoader loaded classes Loader sun/misc/Launcher$AppClassLoader(xDBA) HelloWorld(xACFE) Loader sun/misc/Launcher$ExtClassLoader(xD) Loader *System*(x) java/io/WinNTFileSystem(xCD) java/lang/Throwable(xCA) java/lang/IndexOutOfBoundsException(xD) java/lang/UnsatisfiedLinkError(xDD) classes left out to save space [Ljava/lang/Class;(xCAE) java/io/InputStream(xC) java/lang/Integer$(xCE) java/util/Dictionary(xC) 在这个示例中只有三个标准类装入器 系统类装入器(sun/misc/Launcher$AppClassLoader ) 扩展类装入器(sun/misc/Launcher$ExtClassLoader ) 引导类装入器(*System* ) Classloader 汇总部分提供了系统中每个类装入器的细节在这个系列的文章中感兴趣的类型是基本扩展系统应用程序 和 委托(用在反射中)其他类型(共享的中间件 和信任的)用在 Persistent Reusable JVM 中它们超出了这个文章系列的范围(请参阅 Persistent Reusable JVM User Guide 以获得更多信息在下面的 参考资料 一节中有一个链接)这个汇总部分还显示了父类类装入器系统类装入器的父类是 sun/misc/Launcher$ExtClass loader(xADB)这个父类地址对应于父类类装入器的原始数据结构(叫作 shadow) 类装入器装入的类部分列出了每个类装入器装入的类在这个示例中系统类装入器只装入了一个类 HelloWorld (在地址 xACFE 上) 用 Javadump 解决问题 使用 Javadump 提供的信息可以确定系统中存在哪些类装入器这包括任何用户自定义的类装入器从装入的类列表中可以找出特定的类是由哪个类装入器装入的如果找不到某个类说明系统中的任何一个类装入器都没有装入它(通常会形成 ClassNotFoundException 异常) 可以用 Javadump 诊断的其他类型的问题包括 类装入器命名空间问题 类装入器的命名空间是类装入器和它装入的所有类的组合例如如果某个类存在但是由错误的类装入器装入(有时会造成 NoClassDefFoundError 异常)那么命名空间就是错误的 —— 也就是说类在错误的类路径中为了纠正这种问题可以试着把类放到不同的位置(例如放在正常的 Java 类路径中)并确保由系统类装入器装入它 类装入器约束问题 在这个系列的最后一篇文章中将讨论这种问题的一个示例 Java 方法跟蹤 IBM JVM 有一个内置的方法跟蹤工具这样不需要修改 Java 代码就可以跟蹤任何 Java 代码(包括核心系统)中的方法因为这个工具可以提供大量数据所以可以控制跟蹤的级别只获取需要的信息 启动跟蹤的选项取决于 JVM 的发行版下面是一些命令行示例 在 IBM Java 中运行 HelloWorld 时要跟蹤所有 javalangClassLoader java thods=java/lang/ClassLoader*() Dibmdgtrcprint=mt HelloWorld 跟蹤 ClassLoader 中的 loadClass() 方法和 HelloWorld 中的方法也在 IBM Java 中 java thods=java/lang/ClassLoaderloadClass()HelloWorld*() Dibmdgtrcprint=mt HelloWorld 解释方法跟蹤的输出 这里 是方法跟蹤输出的一个示例(用前面一段的第二个命令行) 跟蹤的每一行都提供了比上面显示的更多的信息我们来完整地看看上面的一行 :: xAC D > java/lang/ClassLoaderloadClass Bytecode method This = xDB Signature: (Ljava/lang/String;Z)Ljava /lang/Class; 这个跟蹤包括 :: 方法进入或退出的时间戳
xAC 线程 ID
D 某些高级诊断使用的内部 JVM 跟蹤点
余下的信息显示是进入(> )还是退出了(< )方法后面跟着方法的细节 用方法跟蹤解决问题 可以用方法跟蹤解决不同类型的问题包括 性能热点 使用时间戳可以发现需要花费相当多时间来执行的方法 挂起 最后的方法项通常是很好的线索可以指明应用程序是否挂起 错误对象 使用地址通过与对象的构建函数调用的地址进行比对可以检查出是不是在正确的对象上调用方法 意外的代码路径 通过跟蹤进入和退出点可以看出程序是否采用了意外的代码路径 其他错误 最后的方法项是对错误发生位置的良好提示 |