类装入问题解密,第 3 部分: 处理更少见的类装入问题
理解类装入并解决微妙的异常
术中心开发团队, IBM Hursley 实验室
2006 年 1 月 16 日
这个四部分构成的文章系列研究 Java™ 的类装入问题,帮助应用程序开发人员理解和调试可能遇到的问题。在第 3 部分中,来自 IBM Hursley 实验室的作者 Lakshmi Shankar 和 Simon Burns 在本系列前两部分的基础之上,详细介绍了不同种类的类装入问题,包括与类路径、类可视性和垃圾收集有关的问题。
本文是本系列中四篇文章的第三篇,它研究了在 Java 开发过程中的一些更复杂、更少见的类装入问题。造成这些问题的原因通常无法直接从症状得出;所以,解决起来既困难又费时。与本系列以前的文章一样,我们仍然提供示例来演示问题,然后讨论各种解决技术。
在开始这篇文章之前,应当熟悉类装入委托模型,以及类链接的阶段和过程。我们强烈建议您从阅读本系列的 第一篇文章 开始。
有一个非常简单的问题通常与用户设置的类路径有关。清单 1 和清单 2 中的示例演示了这个问题。
测试用例创建了两个类装入器,每个类装入器使用的类路径看起来相同。但是,有一个微小但是却重大的区别:一个类路径末尾有 /
,而另一个没有。在这两个类路径中的一个名为 cp 的子目录中有一个类 Z
(在清单 2 中)。两个类装入器都试图装入 Z
:
清单 1. ClasspathTest.java
|
清单 2. Z.java
|
这个测试用例产生以下输出:
|
可以看到,传递给每个 URLClassloader
的参数略有不同。提供给第一个类装入器 cl1
的类路径末尾有 /
。提供给第二个类装入器 cl2
的类路径末尾没有 /
。这个区别是显著的,因为类装入器假设不以 /
结尾的路径指向的是 JAR 文件。只有以 /
结尾的路径才被假定为指向目录。
因为 cl1
的类路径被当作目录,所以这个类装入器能够找到在这个位置的类 Z
,并能够装入它。cl2
的类路径被假定为 JAR 文件;这个类装入器不能发现类 Z
,因为没有这个文件。所以,cl2.loadClass()
抛出 ClassNotFoundException
。
显然,修复这个问题的方法是确保指向目录的路径以 /
结尾。
![]() |
|
在系统中,可能有许多类装入器看不到的类。这是因为类装入器只能看到它自己装入的类,或者它有引用(直接或间接)的其他类装入器装入的类。在标准的类装入委托模型中,类装入器能看到的类被限制在它自己装入的那些类上,或者它的双亲和祖先类装入器装入的类 —— 换句话说,类装入器不能向下看。
图 1 演示了这类问题的示例:
图 1. 可视性示例

类 A
在系统类装入器的类路径中,而 A
的超类 B
,在用户自定义的类装入器的类路径中,这个类装入器是系统类装入器的孩子。当系统类装入器试图装入类 A
时,装入失败,因为它看不到类 B
。这是因为 B
不在系统类装入器或者它的双亲或祖先类装入器的类路径中。
清单 3 到 5 的测试用例实现了这个场景:
清单 3. VisibilityTest.java
|
清单 4. A.java
|
清单 5. B.java
|
这个测试用例产生以下输出:
|
解决类可视性问题的惟一方法是确保所有的应用程序类都可见。为了确保类的可视性而如何确切安放类,取决于使用的不同的类装入模型。但是,如果正在使用标准的类装入委托模型,那么类的可视性就是一个简单的问题:所要做的全部工作就是确保没有引用指向更低的类空间。例如,在系统类装入器类空间中的类,不应当指向孩子或子孙类装入器的类空间中的类。
![]() |
|
如果类装入器只使用标准委托模型,那么就不需要重载 loadClass()
方法。但是,如果需要不同的模型,那么就必须重载 loadClass()
,在这种情况下,必须重视一些特殊的考虑因素。
清单 6 是 loadClass()
的简单实现:
清单 6. 简单的 loadClass() 实现
|
虽然这看起来合理,但是对这个方法的调用会导致以下异常:
|
这个异常的抛出,是因为重载的 loadClass()
方法不再委托给它的双亲。这个实现假设所有需要的类都在这个类装入器的类路径中。这个实现从基本上来说是有缺陷的,因为所有的类(隐式地)都扩展了 java.lang.Object
,而后者必须是引导类装入器所装入的版本。
可以通过修改 loadClass()
的实现来修复这个问题,如清单 7 所示:
清单 7. 改进的 loadClass() 实现
|
方法现在在试图自己找到类之前,先委托给自己的双亲类装入器。这意味着它现在找到了通过引导类装入器装入的 java.lang.Object
。
虽然清单 7 提供的 loadClass()
委托实现解决了这个问题,但是实现仍然是不完整的。在使用这个版本的 loadClass()
时,还会出现另一个问题。下面就是该异常在 IBM JVM 中看起来的样子:
|
下面是在 Sun JVM 中的样子:
|
这个异常发生的原因是,应用程序要求类装入器装入同一个类两次,而 loadClass()
则试图从头开始重新装入类。这造成了两个版本之间的冲突。这个问题可以在 loadClass()
中处理,先检查类装入器的缓存。如果在缓存中发现了类,那么就返回这个版本。这个逻辑被添加到了清单 8 中的 loadClass()
方法版本中:
清单 8. loadClass(),进一步细化
|
这个方法现在工作得很好;但是,它现在遵循的是标准类装入委托(缓存、双亲、磁盘)。当然,如果要求标准委托模型,那么不需要首先重载 loadClass()
。可以编写不符合标准委托模型的有用的 loadClass()
方法,后果是可能会出现潜在的问题,但是这超出了本系列的范围。
![]() |
|
垃圾收集器与类装入器的交互很密切。在众多的事情当中,收集器检查类装入器的数据结构,来判断哪个类是活动的 —— 也就是说,不应当被当作垃圾收集的。这通常会带来一些意料之外的问题。
图 2 演示了一个场景,在这个场景中,序列化以一种意料之外的方式影响了类的垃圾收集(GC):
图 2. 序列化示例

在这个示例中,SerializationTest
实例化了一个 URLClassLoader
,叫做 loader
。在装入 SerializationClass
之后,对类装入器的引用被取消。想法是希望这样可以允许类装入器装入的类被垃圾收集掉。这些类的代码如清单 9 和 10 所示:
清单 9. SerializationTest.java
|
清单 10. SerializationClass.java
|
使用 Javadump,可以发现类装入器是否被垃圾收集了。(关于使用 Javadump 的更多信息,请参阅本系列的第一篇文章。)如果在类装入器的列表中出现以下部分,就说明它没有被收集:
|
虽然取消对用户定义的类装入器的引用看起来像是一种确保类被垃圾收集的方法,但实际并不是这回事。在前面的示例中,由于 java.io.ObjectOutputStream.writeObject(Object obj)
的使用以及它对 GC 的影响,所以产生了问题。
在调用 writeObject()
时(用来序列化 SerializationClass
),对这个类对象的引用就在内部被传递给 ObjectStreamClass
并保存在一个查询表中(也就是内部缓存)。保存这个引用是为了加快日后对同一个类的序列化。
当取消对类装入器的引用时,它装入的类就变成无法进行垃圾收集的了。这是因为在 ObjectStreamClass
查询表中,没有了对 SerializationClass
类的活动引用。ObjectStreamClass
是一个原始类,所以永远不会被垃圾收集。查询表是从 ObjectStreamClass
中的静态字段引用的,而且保存在类本身之中,而不是保存在实例中。所以,对 SerializationClass
的引用存在于 JVM 的生命周期中,所以类就不能被垃圾收集。重要的是,SerializationClass
类有一个到其定义类装入器的引用,所以它也不可能完整地取消引用。
为了避免这个问题,凡是要进行序列化的类,都应当由不需要被垃圾收集的类装入器装入 —— 例如由系统类装入器装入。
![]() |
|
在这篇文章中,学习了一些在类装入中可能发生的更复杂的问题。在本系列的最后一篇文章中,将研究两个可能发生的最复杂的问题:死锁和违反约束。
![]() |
|
学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文。
- Demystifying class loading problems:阅读完整系列。
- “了解 Java ClassLoader”(Greg Travis developerWorks,2001 年 4 月):类装入介绍。
- “Java 编程的动态性,第 1 部分: 类和类装入”(Dennis Sosnoski developerWorks,2003 年 4 月):理解各种类装入问题,范围从运行简单 Java 应用程序所需要的大量的类一直到可能在 J2EE 和类似的复杂架构中造成问题的类装入器冲突。
- JVM specification:从起源开始全面介绍了 JVM 类文件的格式和指令集。
- IBM Diagnostics Guides:学习关于调试的更多知识。
- Persistent Reusable JVM:在线图书,介绍 Persistent Reusable JVM 的概念,并提供编写在它们上面运行的中间件和应用程序的技术指南。
- Java 技术专区:数百份 Java 编程各方面的文章。
获得产品和技术
- IBM Java developer kits:在 IBM 的一些最流行的平台上创建和运行 J2SE 应用程序。
讨论
- 加入本文的论坛 。(您也可以通过点击文章顶部或者底部的论坛链接参加讨论。)
- developerWorks blogs:加入 developerWorks 社区。
![]() |
|
![]() | |
| Lakshmi Shankar 是英国 IBM Hursley 实验室的软件工程师。他为 IBM 工作超过两年了,有广泛的经验,一直在 Hursley 实验室从事 Java 性能、测试和开发工作。他目前是 IBM Java 技术的类装入组件的所有人。他现在是信息管理团队的一名开发人员。 |
![]() | |
| Simon Burns 是 Shiraz(可重置 JVM 和 IBM Java 共享类)组件的所有人,也是 IBM Hursley 实验室的 Java 技术团队负责人。他在 JVM 开发上工作了三年,专攻 Shiraz 组件和 z/OS 平台。他和 CICS 紧密合作,帮助他们利用这项技术。Simon 开发的 OSGi 框架是开放源码的 Eclipse Equinox 项目的一部分,已经集成到 Eclipse 3.1 中。他现在正在进行组件化的工作。 |