您的位置:首页 > 编程语言 > Java开发

类装入问题解密,第 4 部分: 死锁和约束

2006-01-17 17:24 351 查看

类装入问题解密,第 4 部分: 死锁和约束

深入观察两个最复杂的类装入问题
文档选项
将此页作为电子邮件发送
对此页的评价
帮助我们改进这些内容
级别: 中级Simon Burns , Java 技术中心开发团队, IBM Hursley 实验室Lakshmi Shankar , Java 技术中心开发团队, IBM Hursley 实验室2006 年 1 月 16 日这个四部分构成的文章系列研究 Java™ 的类装入问题,帮助应用程序开发人员理解和调试可能遇到的问题。在最后这一期中,来自 IBM Hursley 实验室的作者 Lakshmi Shankar 和 Simon Burns 在本系列前三部分的基础之上,研究这个领域中可能遇到的两个最有趣和最复杂的问题:死锁和约束。本文是本系列中的四篇文章的最后一篇,它研究了类装入器死锁和约束违反。这两类问题不仅难于理解,更难解决。与本系列中以前的文章中一样,我们还是提供示例来演示问题,然后讨论各种解决技术。在开始这篇文章之前,应当熟悉类装入委托模型,以及类链接的阶段和过程。我们强烈建议您从阅读本系列的 第一篇文章 开始。类装入器死锁当两个线程在两个不同的类装入器上都拥有各自的锁,同时又都等候对方拥有的锁的时候,就会发生类装入器死锁。两个线程都会无限期地等候另一个类装入器上的锁,所以它们就变成了死锁的。这些死锁可以发生在多线程环境中当常用的委托模型被忽略时。请考虑图 1 所描述的情况:图 1. 类装入器死锁示例在这里有两个用户定义的类装入器,即
mcl1
mcl2
mcl1
是系统类装入器的孩子,
mcl2
mcl1
的孩子。类
A
B
mcl1
的类路径上,而类
C
mcl2
的类路径上。类
A
扩展了
C
,类
C
扩展了
B
。一般在这种情况下,在试图装入
C
A
的超类)时,
mcl1
会抛出
NoClassDefFoundError
,因为
mcl1
不能向下看,而
C
只能被
mcl1
下面的类装入器装入。但是,在这种特殊的情况下,
mcl1
向下委托它的孩子类装入器去装入特定包(
package2
)中的类,而类
C
就在这个包中。清单 1 到 6 的测试用例实现了这个场景:清单 1. ClassLoaderDeadlockTest.java
清单 2. MyClassLoader1.java
清单 3. MyClassLoader2.java
清单 4. package1/A.java
清单 5. package1/B.java
清单 6. package2/C.java
在运行以上测试用例时,生成以下输出,然后应用程序就挂起:
应用程序之所以挂起,是因为每个线程都拥有一个类装入器上的锁,并想得到另一个类装入器上的锁,如图 2 的时间线图所示:图 2. 类装入器死锁时间线线程 2(t2)首先调用
mcl2
上同步的
loadClass()
方法以装入
package2.C
,这造成 t2 得到
mcl2
上的锁。然后线程 1 (t1)启动,并调用
mcl1
上的
loadClass()
以装入
package1.A
,这造成 t1 得到
mcl1
上的锁。因为
package1.A
扩展了
package2.C
,所以
mcl1
开始装入超类。因为
C
package2
中,所以
mcl1
向下委托
mcl2
进行装入,就像前面描述的那样。这造成 t1 请求
mcl2
上的锁,并一直等候到可以得到锁为止。现在 t2 想用
mcl1
装入
package2.C
的超类(即
package1.B
),因而想要得到
mcl1
上的锁。因为每个线程都在等候对方持有的锁,所以就发生了死锁。这种性质的死锁可以用本系列的第一篇文章中描述的某些调试特性来解决。在运行这个程序时设置 IBM 的 Verbose 类装入选项(
-Dibm.cl.verbose
),会有助于理解导致这个死锁的类装入顺序。这里是输出。为了让这个清单更容易阅读,t2 的输出用粗体文本表示,t1 的输出用正常文本。可以看出,t2 到达的点已经装入了类
C
,而 t1 已经装入了类
A
。这个问题最有价值的信息可以在 Javadump 中找到,用本系列的 第 1 部分 中描述的机制获得。JVM 通常能够探测到已经发生的死锁,并在 Javadump 中报告死锁,如下所示。(在这里,t2 被标识为
main
,t1 被标识为
Thread-0
):
这一节向我们展示了死锁中包含的线程,以及它们持有的锁和正在等候的锁。就在这一节下面,Javadump 显示了这些线程在死锁时的堆栈跟踪。不出所料,两个类装入器都在试图装入类:
Javadump 还显示了这些类装入器装入的类:
有了这些信息,就应当可以解决死锁问题了。因为死锁的类装入器的标识已知,所以可以检查这些类装入器使用的委托模型。在本例中,委托模型是一个正确的图(带有循环),所以死锁的发生可能是使用特定的类关系和线程的结果。在这里,类关系是类
A
扩展了类
C
,从而触发了循环的委托模型。
回页首
类装入器约束违反类装入器约束保证了类空间在 JVM 中的一致性。换句话说,当两个类装入器用相同的名称装入不同的类时(也就是不同的字节码),类装入器约束保证了它们之间不会有类型不匹配。根据 JVM 规范,当满足以下四个条件时,就违反了类装入器约束:有一个类装入器
L
,Java 虚拟机把
L
记录成名为
N
的类
C
的初始装入器有一个类装入器
L'
,Java 虚拟机把
L'
记录成名为
N
的类
C'
的初始装入器实施的约束集合(的传递封包)所定义的等价关系表明:
N L = N L'
C != C'
解释这些条件的最简单方法是用一个示例。请考虑图 3 的场景:图 3. 类装入器约束
A
有一个静态方法
methodA()
,它用类
C
的实例作为参数。类
B
有一个静态方法
methodB()
,它调用类
A
中的
methodA()
,以
C
的一个实例作为参数。主程序调用类
B
中的
methodB()
。现在把这与 JVM 规范中定义的四个条件关联起来:
L = mycl1
C
=
mycl1
装入的类
C
N = C
L' = mycl2
C'
=
mycl2
装入的类
C
N = C
。从
B
A
的方法调用中,传递了
C
的一个实例,这一传递所隐含的约束建立了等价关系。类
C
!= 类
C'
因为四个条件全部成立,所以这种情况会导致类装入器约束违反。清单 7 到 12 的测试用例实现了这个场景:清单 7. ConstraintViolationTest.java
清单 8. MyClassLoader1.java
清单 9. MyClassLoader2.java
清单 10. A.java
清单 11. B.java
清单 12. C.java
mcl1
mcl2
的类路径中,都必须放入类
C
的一个副本。这个测试用例产生 以下输出解决类装入器约束违反许多开发人员发现约束违反是一种很难解决的类装入问题。这主要是因为对于第一次遇到这个问题的开发人员来说,异常的消息看起来可能很奇怪。解决这个问题的一个良好起点是,检查包含的类。可以从 IBM 的冗余输出或从 Javadump 中确认这些信息。从上面的输出中,可以看出违反装入器约束的类是
C
。如果想使用 IBM 的冗余输出检查包含的类,应当使用命令行选项
-Dibm.cl.verbose=C
。输出会显示两个装入
C
的不同类装入器。查看这个问题的更清楚的方式是生成 Javadump。这个场景的 Javadump 的类装入区看起来应当像这样:
可以看到,类
C
已经由
MyClassLoader1
的实例(
mcl1
)装入,还由
MyClassLoader2
的实例(
mcl2
)装入。重要的是,两个类的地址(在括号中显示)不同。这意味着字节码来自不同的文件。解决这个问题的最简单方法是确保在系统中只有类的一个副本 —— 也就是说,类只出现在一个类装入器的类路径中。但是,如果有必要拥有同一个类的两个副本,那么重要的是要确保在引用它们的类之间没有交互。避免类装入器约束违反虽然避免类装入器约束违反的最简单方法是在系统中只有类的一个副本,但有时拥有多个版本也是有必要的。在部署类的多个版本时避免约束违反的一个可行方法是,使用对等类装入 模型,如图 4 所示。对等类装入不遵循传统的类装入器层次委托结构。相反,它有一组类装入器,彼此互不相关,但是有共同的双亲(通常是系统类装入器)。这些类装入器不仅可以委托给它们的双亲,还能委托给它们的对等体。图 4. 对等类装入这类类装入器结构允许在一个 JVM 中存在离散的类空间;所以,对于运行组件化的产品来说非常有用。这种类装入结构的示例就是 OSGi 框架,例如 Eclipse 构建于其上的框架。
回页首
结束语本系列对使用 Java 类装入器时可能遇到的潜在问题提供了一般性的概述。我们介绍了可能发生的不同种类的异常,以及如何解决它们。我们还研究了在使用隐式或显式类装入器时可能出现的其他一些问题。另外,我们还介绍了 IBM JVM 的各种调试特性,并介绍了如何把它们应用到各种问题上。我们希望这些文章提供的知识能够让您更好地理解类装入,并在应用程序中更好地利用类装入器。
回页首
参考资料 学习您可以参阅本文在 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 社区。
回页首
作者简介
Simon Burns 是 Shiraz(可重置 JVM 和 IBM Java 共享类)组件的所有人,也是 IBM Hursley 实验室的 Java 技术团队负责人。他在 JVM 开发上工作了三年,专攻 Shiraz 组件和 z/OS 平台。他和 CICS 紧密合作,帮助他们利用这项技术。Simon 开发的 OSGi 框架是开放源码的 Eclipse Equinox 项目的一部分,已经集成到 Eclipse 3.1 中。他现在正在进行组件化的工作。
Lakshmi Shankar 是英国 IBM Hursley 实验室的软件工程师。他为 IBM 工作超过两年了,有广泛的经验,一直在 Hursley 实验室从事 Java 性能、测试和开发工作。他目前是 IBM Java 技术的类装入组件的所有人。他现在是信息管理团队的一名开发人员。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息