本文是该系列文章的最后四部分,它探讨了类加载器的死锁和约束违规。 这两种类型的问题可能很难理解,而很难解决。 就像我们在本系列前几篇文章中所做的一样,我们提供了一些示例来说明问题,然后继续讨论各种分辨率技术。
在开始本文之前,您应该熟悉类加载器委托模型,以及类链接的阶段和阶段。 我们建议您先阅读本系列的第一篇文章 。
类加载器死锁
当两个线程各自在两个不同的类加载器上拥有一个锁,并且两个线程都在等待另一个线程拥有的锁时,就会发生类加载器死锁 。 两个线程将无限期地等待另一个类加载器上的锁定,因此它们陷入死锁状态。 当绕过通常的委派模型时,这些死锁可能会在多线程环境中发生。 考虑图1概述的情况:
图1.类加载器死锁示例
![类加载器死锁示例](https://i-blog.csdnimg.cn/blog_migrate/a7994696260d4d5c5771228c3e079957.png)
这里我们有两个用户定义的类加载器, mcl1
和mcl2
。 mcl1
是系统类加载器的子级,而mcl2
是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
import java.net.URL;
public class ClassLoaderDeadlockTest {
MyClassLoader1 mycl1;
MyClassLoader2 mycl2;
public static void main(String[] args) {
new ClassLoaderDeadlockTest().test();
}
public void test() {
try {
mycl1 = new MyClassLoader1(new URL[] { new URL(
"file://C:/CL_Article/ClassloaderDeadlocks/cp1/") });
mycl2 = new MyClassLoader2(new URL[] { new URL(
"file://C:/CL_Article/ClassloaderDeadlocks/cp2/") }, mycl1);
} catch (Exception e) {
e.printStackTrace();
}
Thread t1 = new Thread(new Runnable() {
public void run() {
try {
System.out.println("About to load class A with mycl1");
mycl1.loadClass("package1.A");
System.out.println("Loaded Class A with mycl1");
} catch (Exception e) {
e.printStackTrace();
}
}
});
t1.start();
try {
System.out.println("About to load class C with mycl2");
mycl2.loadClass("package2.C");
System.out.println("Loaded Class C with mycl2");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
清单2. MyClassLoader1.java
import java.net.URL;
import java.net.URLClassLoader;
public class MyClassLoader1 extends URLClassLoader {
MyClassLoader1(URL[] urls) {
super(urls);
}
public Class loadClass(String name) throws ClassNotFoundException{
if (name.startsWith("package2."))
return MyClassLoader2.getClassLoader().loadClass(name);
else
return findClass(name);
}
}
清单3. MyClassLoader2.java
import java.net.URL;
import java.net.URLClassLoader;
public class MyClassLoader2 extends URLClassLoader{
static ClassLoader loader;
MyClassLoader2(URL[] urls, ClassLoader parent) {
super(urls, parent);
loader = this;
}
public static ClassLoader getClassLoader() {
return loader;
}
}
清单4. package1 / A.java
package package1;
public class A extends package2.C {
}
清单5. package1 / B.java
package package1;
public class B {
}
清单6. package2 / C.java
package package2;
public class C extends package1.B {
}
当运行上述测试用例时,将产生以下输出,然后应用程序挂起:
About to load class C with mycl2
About to load class A with mycl1
该应用程序挂起是因为每个线程在一个类加载器上拥有一个锁,并希望在另一个类加载器上拥有该锁,如图2的时间线图所示:
图2.类加载器死锁时间线
![类加载器死锁时间轴](https://i-blog.csdnimg.cn/blog_migrate/b84f3b12be33ad6792ac845c912dd8b1.png)
线程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
)将有助于您了解导致此死锁的类加载顺序。 这是输出 。
IBM Verbose类装入选项输出
About to load class C with mycl2
ExtClassLoader attempting to find package2.C
...
About to load class A with mycl1
MyClassLoader1 attempting to find package1.A
MyClassLoader1 using classpath \C:\CL_Article\ClassloaderDeadlocks\cp1
...
ExtClassLoader could not find package2.C
...
AppClassLoader attempting to find package2.C
...
AppClassLoader could not find package2.C
...
MyClassLoader1 attempting to find package2.C
MyClassLoader1 using classpath \C:\CL_Article\ClassloaderDeadlocks\cp1
MyClassLoader1 found package1/A.class in \C:\CL_Article\ClassloaderDeadlocks\cp1
MyClassLoader1 could not find package2/C.class in \C:\CL_Article\ClassloaderDeadlocks\cp1
MyClassLoader1 could not find package2.C
...
MyClassLoader2 attempting to find package2.C
MyClassLoader2 using classpath \C:\CL_Article\ClassloaderDeadlocks\cp2
MyClassLoader2 found package2/C.class in \C:\CL_Article\ClassloaderDeadlocks\cp2
为了使此清单更易于阅读,t2的输出以粗体显示,t1的输出为普通文本。 如您所见,t2已经达到加载类C
的地步,而t1已经加载类A
的地步。
可以使用本系列第1部分中描述的机制在Javadump中找到有关此问题的最有价值的信息。 JVM通常可以检测到发生了死锁,并在Javadump中报告该死锁,如下所示。 (这里,t2被标识为main
,t1被标识为Thread-0
):
...
Deadlock detected!!!
---------------------
Thread "Thread-0" (0x44DFE1E8)
is waiting for:
sys_mon_t:0x002A26D0 infl_mon_t: 0x00000000:
MyClassLoader2@ADB658/ADB660:
which is owned by:
Thread "main" (0x2A1750)
which is waiting for
sys_mon_t:0x002A2718 infl_mon_t: 0x00000000:
MyClassLoader1@ADB6D8/ADB6E0:
which is owned by:
Thread "Thread-0" (0x44DFE1E8)
...
本节向我们展示了死锁中涉及的线程以及它们所持有和等待的锁。 在本节的下面,Javadump显示了死锁时这些线程的堆栈跟踪。 不出所料,两个类加载器都试图加载一个类:
"Thread-0" (TID:0xADB600, sys_thread_t:0x44DC72D0, state:CW, native ID:0x9DC) prio=5
at java.lang.ClassLoader.loadClass(ClassLoader.java:577)
at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
at MyClassLoader1.loadClass(MyClassLoader1.java:12)
at java.lang.ClassLoader.defineClass0(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:810)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:147)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:475)
at java.net.URLClassLoader.access$500(URLClassLoader.java:109)
at java.net.URLClassLoader$ClassFinder.run(URLClassLoader.java:848)
at java.security.AccessController.doPrivileged1(Native Method)
at java.security.AccessController.doPrivileged(AccessController.java:389)
at java.net.URLClassLoader.findClass(URLClassLoader.java:371)
at MyClassLoader1.loadClass(MyClassLoader1.java:13)
at ClassLoaderDeadlockTest$1.run(ClassLoaderDeadlockTest.java:29)
at java.lang.Thread.run(Thread.java:568)
"main" (TID:0xADB9B8, sys_thread_t:0x2A2028, state:CW, native ID:0x18C) prio=5
at java.lang.ClassLoader.loadClass(ClassLoader.java:577)
at java.lang.ClassLoader.loadClass(ClassLoader.java:563)
at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
at java.lang.ClassLoader.defineClass0(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:810)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:147)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:475)
at java.net.URLClassLoader.access$500(URLClassLoader.java:109)
at java.net.URLClassLoader$ClassFinder.run(URLClassLoader.java:848)
at java.security.AccessController.doPrivileged1(Native Method)
at java.security.AccessController.doPrivileged(AccessController.java:389)
at java.net.URLClassLoader.findClass(URLClassLoader.java:371)
at java.lang.ClassLoader.loadClass(ClassLoader.java:572)
at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
at ClassLoaderDeadlockTest.test(ClassLoaderDeadlockTest.java:42)
at ClassLoaderDeadlockTest.main(ClassLoaderDeadlockTest.java:10)
Javadump还显示了这些类加载器加载的类:
ClassLoader loaded classes
Loader MyClassLoader2(0x452DA0F0)
package2/C(0x00ACEAF0)
Loader MyClassLoader1(0x452DA7F8)
package1/A(0x00ACEC20)
Loader sun/misc/Launcher$AppClassLoader(0x44D7C7B8)
ClassLoaderDeadlockTest$1(0x00ACED50)
MyClassLoader1(0x00ACEFB0)
ClassLoaderDeadlockTest(0x00ACF0E0)
MyClassLoader2(0x00ACEE80)
Loader sun/misc/Launcher$ExtClassLoader(0x44D73D78)
Loader *System*(0x00352A08)
sun/reflect/UnsafeFieldAccessorFactory(0x44D40998)
java/lang/Class$1(0x002CF128)
java/io/InputStream(0x002C9818)
java/lang/Integer$1(0x002C83E8)
...
利用所有这些信息,应该可以解决死锁。 因为已知死锁类装入器的标识,所以可以检查这些类装入器正在使用的委托模型。 在这种情况下,委托模型是一个合适的图形(带有周期),因此,由于特定的类关系和线程使用情况,可能会发生死锁。 在这里,扩展了类C
的类A
的类关系触发了循环委托模型。
类加载器约束违规
类加载器约束可确保JVM中类空间的一致性。 换句话说,当两个类加载器以相同的名称加载不同的类(即,不同的字节码)时,类加载器约束保证了它们之间不会存在类型不匹配。
根据JVM规范,当满足以下四个条件时,将违反类加载器约束:
- 存在一个加载程序
L
,使得Java虚拟机已将L
记录为名为N
C
类的启动加载程序- 存在一个加载程序
L'
这样Java虚拟机已将L'
记录为名为N
的类C'
的初始加载程序- 由(强制传递的)约束集合定义的等价关系表示
NL = N L'
C != C'
解释这些情况的最简单方法是举一个例子。 考虑图3中的场景:
图3.类加载器约束
![类加载器约束](https://i-blog.csdnimg.cn/blog_migrate/115ede1188cc11b4e19089265686ee4d.png)
类A
具有一个静态方法methodA()
,该方法将类C
的实例作为参数。 类B
有一个静态方法, methodB()
调用methodA()
类A
,传入的实例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
import java.lang.reflect.Method;
import java.net.URL;
public class ConstraintViolationTest {
MyClassLoader1 mycl1;
MyClassLoader2 mycl2;
public static void main(String[] args) {
new ConstraintViolationTest().test();
}
public void test() {
try {
mycl1 = new MyClassLoader1(new URL[] { new URL(
"file://C:/CL_Article/ConstraintViolation/cp1/") });
mycl2 = new MyClassLoader2(new URL[] { new URL(
"file://C:/CL_Article/ConstraintViolation/cp2/") }, mycl1);
System.out.println("About to load class A with mycl1");
mycl2.loadClass("A");
System.out.println("Loaded Class A with mycl1");
System.out.println("About to load class B with mycl2");
Class myB = mycl2.loadClass("B");
Method aMethod = myB.getMethod("methodB", new Class[] {});
aMethod.invoke(null, new Object[] {});
System.out.println("Loaded Class B with mycl2");
} catch (Exception e) {
e.printStackTrace();
}
}
}
清单8. MyClassLoader1.java
import java.net.URL;
import java.net.URLClassLoader;
public class MyClassLoader1 extends URLClassLoader {
MyClassLoader1(URL[] urls) {
super(urls);
}
}
清单9. MyClassLoader2.java
import java.net.URL;
import java.net.URLClassLoader;
public class MyClassLoader2 extends URLClassLoader {
static ClassLoader loader;
MyClassLoader2(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public Class loadClass(String name) throws ClassNotFoundException {
Class aClass = findLoadedClass(name);
if (aClass != null)
return aClass;
if (name.startsWith("C"))
return findClass(name);
else
return super.loadClass(name);
}
}
清单10. A.java
public class A extends C {
public static void methodA(C c){
}
}
清单11. B.java
public class B extends C {
static A a = new A();
public static void methodB() {
A.methodA(new C());
}
}
清单12. C.java
public class C {
}
必须将类C
的副本放置在mcl1
和mcl2
的类路径上。 该测试用例产生以下输出 。
测试用例输出
About to load class A with mycl1
Loaded Class A with mycl1
About to load class B with mycl2
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:85)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:58)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:60)
at java.lang.reflect.Method.invoke(Method.java:391)
at ConstraintViolationTest.test(ConstraintViolationTest.java:31)
at ConstraintViolationTest.main(ConstraintViolationTest.java:11)
Caused by: java.lang.LinkageError: Class C violates loader constraints: definition mismatch between parent and child loaders
at B.methodB(B.java:7)
... 7 more
解决类加载器约束冲突
许多开发人员发现违反约束是很难解决的类加载问题。 这主要是因为异常消息对于第一次遇到此问题的开发人员而言似乎是难以理解的。
解决此问题的一个很好的起点是检查所涉及的类。 这些可以从IBM详细输出或从Javadump确定。
从上面的输出中,您可以看到违反加载程序约束的类是C
如果要使用IBM verbose输出检查所涉及的类,则应使用命令行选项-Dibm.cl.verbose=C
输出将显示两个不同的类加载器,它们加载C
解决此问题的更清晰方法是生成Javadump。 在这种情况下,Javadump的类加载部分应如下所示:
ClassLoader loaded classes
Loader MyClassLoader2(0x44DC93A8)
C(0x00ACE9C0)
B(0x00ACEAF0)
Loader MyClassLoader1(0x44DC64B8)
C(0x00ACEC20)
A(0x00ACED50)
Loader sun/misc/Launcher$AppClassLoader(0x44D7C7B8)
MyClassLoader1(0x00ACEFB0)
ConstraintViolationTest(0x00ACF0E0)
MyClassLoader2(0x00ACEE80)
Loader sun/misc/Launcher$ExtClassLoader(0x44D73D78)
Loader *System*(0x00352A08)
sun/net/TransferProtocolClient(0x44D4AB18)
sun/reflect/UnsafeFieldAccessorFactory(0x44D40998)
java/lang/Class$1(0x002CF128)
java/io/InputStream(0x002C9818)
...
如您所见,类C
已由MyClassLoader1
的实例( mcl1
)和MyClassLoader2
的实例( mcl2
) mcl2
。 重要的是,这两个类别的地址(显示在括号中)是不同的。 这意味着字节码来自不同的文件。
解决此问题的最简单方法是,确保系统中只有一个类的副本–即,该类仅出现在一个加载器的类路径中。 但是,如果必须有两个相同类的副本,那么确保引用它们的任何类之间没有交互是很重要的。
避免违反类加载器约束
避免违反类加载器约束的最简单方法是在系统中仅拥有一个类的一个副本,但有时必须具有多个版本。
避免约束冲突的一种可能方法是在仍然部署一个类的多个版本的同时,使用对等类加载模型,如图4所示。对等类加载不遵循类加载器的传统分层委托结构。 相反,它具有一组不相关的类加载器,只是它们具有相同的父级(通常是系统类加载器)。 这些类加载器不仅可以委派给其父级,还可以委派给其同级。
图4.对等类加载
![对等类加载](https://i-blog.csdnimg.cn/blog_migrate/44fc34d7a102aa4fd12de24c535e27a0.png)
这种类加载器结构允许离散的类空间存在于一个JVM中。 因此,这对于运行组件化产品非常有用。 这种类加载结构的一个很好的例子是OSGi框架,例如构建在上面的Eclipse。
结论
本系列文章概述了使用Java类装入器时可能遇到的潜在问题。 我们向您展示了可能发生的各种异常以及如何解决它们。 我们还研究了在隐式和显式使用类加载器时可能出现的其他一些问题。 此外,我们介绍了IBM JVM的各种调试功能,并展示了如何将其应用于各种问题。
我们希望这些文章提供的见解将使您能够更好地理解类加载,并在应用程序中更好地使用类加载器。
翻译自: https://www.ibm.com/developerworks/java/library/j-dclp4/index.html