本文是本系列文章(共四篇)中的第三篇,探讨了在Java开发过程中可能遇到的一些更复杂和异常的类加载问题。 从症状上看,这些问题的原因并不总是很明显。 结果,解决这些问题可能既困难又耗时。 与本系列中的前几篇文章一样,我们提供了一些示例来说明问题,然后讨论了各种解决方法。
在开始本文之前,您应该熟悉类加载器委托模型,以及类链接的阶段和阶段。 我们建议您先阅读本系列的第一篇文章 。
与类路径有关的问题
有一个非常简单的问题,通常会吸引用户设置类路径。 清单1和2中的示例说明了此问题。
该测试用例创建了两个类加载器,每个类加载器似乎都具有相同的类路径。 但是,实际上有一个小的区别是很重要的:一个类路径具有尾随/
,而另一个则没有。 在两个类路径上的一个名为cp的子目录中有一个类Z
(如清单2所示)。 两个类加载器都将尝试加载Z
:
清单1. ClasspathTest.java
import java.net.URL;
import java.net.URLClassLoader;
public class ClasspathTest {
String userDir;
URL withSlash;
URL withoutSlash;
ClasspathTest() {
try {
userDir = System.getProperty("user.dir");
withSlash = new URL("file://C:/CL_Article/ClasspathIssues/cp/");
withoutSlash = new URL("file://C:/CL_Article/ClasspathIssues/cp");
} catch (Exception e) {
e.printStackTrace();
}
}
void run() {
try {
System.out.println(withSlash);
URLClassLoader cl1 = new URLClassLoader(new URL[] { withSlash });
Class c1 = cl1.loadClass("Z");
System.out.println("Class Z loaded.");
System.out.println(withoutSlash);
URLClassLoader cl2 = new URLClassLoader(new URL[] { withoutSlash });
Class c2 = cl2.loadClass("Z");
System.out.println("Class Z loaded.");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new ClasspathTest().run();
}
}
清单2. Z.java
public class Z {
}
该测试用例产生以下输出:
file://C:/CL_Article/ClasspathIssues/cp/
Class Z loaded.
file://C:/CL_Article/ClasspathIssues/cp
java.lang.ClassNotFoundException: Z
at java.net.URLClassLoader.findClass(URLClassLoader.java:376)
at java.lang.ClassLoader.loadClass(ClassLoader.java:572)
at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
at ClasspathTest.run(ClasspathTest.java:28)
at ClasspathTest.main(ClasspathTest.java:36)
如您所见,每个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
import java.net.*;
public class VisibilityTest {
public static void main(String[] args) {
try {
URLClassLoader mycl = new URLClassLoader(new URL[] { new URL(
"file://C:/CL_Article/VisibilityTest/cp/") });
Class c2 = mycl.loadClass("A");
} catch (Exception e) {
e.printStackTrace();
}
}
}
清单4. A.java
public class A extends B {
public static void method1() {
System.out.println("HELLO!");
}
}
清单5. B.java
public class B {
}
该测试用例产生以下输出:
Exception in thread "main" java.lang.NoClassDefFoundError: B
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 sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:442)
at java.lang.ClassLoader.loadClass(ClassLoader.java:563)
at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
at VisibilityTest.main(VisibilityTest.java:9)
at VisibilityTest.main(VisibilityTest.java:10)
解决类可见性问题的唯一方法是确保所有应用程序类都是可见的。 您如何精确地放置类以确保其可见性取决于您所使用的类加载模型。 但是,如果您使用标准的类加载委托模型,则类可见性是一个简单的问题:所需要做的就是确保没有指向较低类空间的引用。 例如,系统类加载器类空间中的类不应引用子类或后代类加载器的类空间中的类。
覆盖loadClass()时的问题
如果类加载器仅使用标准的委托模型,则无需重写loadClass()
方法。 但是,如果需要其他模型,则必须重写loadClass()
,在这种情况下,必须考虑一些特殊的注意事项。
代表团
清单6是loadClass()
的简单实现:
清单6.一个简单的loadClass()实现
public Class loadClass(String name) throws ClassNotFoundException {
return findClass(name);
}
尽管这看起来很合理,但是调用此方法会导致以下异常:
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
抛出此异常是因为重写的loadClass()
方法从不委托给其父级。 此实现假定所需的所有类都在此类加载器的类路径上。 此实现从根本上来说是有缺陷的,因为所有类(隐式地)都扩展了java.lang.Object
,该类必须是引导类加载器加载的版本。
可以通过修改loadClass()
的实现来解决此问题,如清单7所示:
清单7.改进的loadClass()实现
public Class loadClass(String name) throws ClassNotFoundException {
Class c = null;
try {
c = getParent().loadClass(name);
} catch (ClassNotFoundException e) {
}
if(c == null)
c = findClass(name);
return c;
}
现在,该方法在尝试查找类本身之前将其委托给其父类加载器。 这意味着它现在可以找到由引导类加载器加载的java.lang.Object
。
快取
清单7中给出的委托委托loadClass()
实现解决了该问题,但实现仍不完整。 使用此版本的loadClass()
时,可能会发生另一个错误。 这是IBM JVM上的异常情况:
Exception in thread "main" java.lang.LinkageError:
JVMCL048:redefine of class A (&name=44CA3B08). old_cb=ACEE80,
new_cb=ACED50, (&old_name=44CA3B08) old_name=A
这是在Sun JVM上:
Exception in thread "main" java.lang.LinkageError: duplicate class definition: A
发生此异常是因为应用程序要求类加载器两次加载同一类,并且loadClass()
尝试从头开始重新加载它。 这会导致两个版本之间发生冲突。 这可以通过首先检查类加载器的缓存在loadClass()
进行处理。 如果在缓存中找到该类,则应返回该版本。 此逻辑已添加到清单8的loadClass()
方法的版本中:
清单8.进一步完善的loadClass()
public Class loadClass(String name) throws ClassNotFoundException {
Class c = findLoadedClass(name);
if(c == null) {
try {
c = getParent().loadClass(name);
} catch (ClassNotFoundException e) {
}
if(c == null)
c = findClass(name);
}
return c;
}
这种方法现在可以正常工作了。 但是,它现在遵循标准的类加载委托(缓存,父级,磁盘)。 当然,如果需要标准的委托模型,则无需首先覆盖loadClass()
。 可以编写不遵循标准委托模型的有用的loadClass()
方法。 结果可能会出现一些问题,但是这些问题不在本系列的讨论范围之内。
与垃圾回收和序列化有关的问题
垃圾收集器与类加载器紧密交互。 除其他事项外,收集器检查类加载器的数据结构,以确定哪些类是活动的 -即不可垃圾收集。 这通常会导致一些意外的问题。
图2说明了序列化以意外的方式影响类和类加载器的垃圾回收(GC)的情况:
图2.序列化示例
在此示例中, SerializationTest
实例化一个URLClassLoader
,称为loader
。 加载SerializationClass
,将取消对类加载器的引用。 期望这将允许对其加载的类进行垃圾回收。 清单9和清单10展示了这些类的代码:
清单9. SerializationTest.java
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class SerializationTest extends ClassLoader {
public static void main(String args[]) {
try {
URLClassLoader loader = new URLClassLoader(new URL[] { new URL(
"file://C:/CL_Article/Serialization/dir1/") });
System.out.println("Loading SerializationClass");
Class c = loader.loadClass("SerializationClass");
System.out.println("Creating an instance of SerializationClass");
c.newInstance();
System.out.println("Dereferencing the class loader");
c = null;
loader = null;
System.out.println("Running GC...");
System.gc();
System.out.println("Triggering a Javadump");
com.ibm.jvm.Dump.JavaDump();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
清单10. SerializationClass.java
import java.io.File;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SerializationClass implements Serializable {
private static final long serialVersionUID = 5024741671582526226L;
public SerializationClass() {
try {
File file = new File("C:/CL_Article/Serialization/test.txt");
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(this);
oos.reset();
oos.close();
fos.close();
oos = null;
fos = null;
file = null;
} catch (Exception e) {
e.printStackTrace();
}
}
}
使用Javadump,可以发现类加载器是否已被垃圾回收。 (有关使用Javadump的更多信息,请参阅本系列的第一篇文章。)如果以下部分出现在类加载器的列表中,则说明它尚未被收集:
------a- Loader java/net/URLClassLoader(0x44DC6DE0), Shadow 0x00ADB6D8,
Parent sun/misc/Launcher$AppClassLoader(0x00ADB7B0)
Number of loaded classes 1
Number of cached classes 11
Allocation used for loaded classes 1
Package owner 0x00ADB6D8
尽管取消引用用户定义的类加载器似乎是确保类被垃圾回收的一种方法,但实际上并非如此。 在前面的示例中,问题源于java.io.ObjectOutputStream.writeObject(Object obj)
的使用及其对GC的影响。
调用writeObject()
(以序列化SerializationClass
)时,对此类对象的引用在内部传递给ObjectStreamClass
并存储在查找表(即内部缓存)中。 保留此参考信息以加快将来同一类的序列化。
取消引用类加载器时,其加载的类不可回收。 这是因为从ObjectStreamClass
查找表中实时引用了SerializationClass
类。 ObjectStreamClass
是原始类,因此永远不会进行垃圾回收。 查找表是从ObjectStreamClass
的静态字段引用的,并保留在类本身而不是其实例中。 结果,在JVM的整个生命周期中都存在对SerializationClass
的引用,因此无法对该类进行垃圾回收。 重要的是, SerializationClass
类具有对其定义的类加载器的引用,因此也不能完全取消引用它。
为避免此问题,任何要序列化的类都应由不需要进行垃圾回收的类加载器加载-例如,由系统类加载器加载。
下一步是什么
在本文中,您了解了类加载中可能发生的一些更复杂的问题。 在本系列的最后一篇文章中,我们将研究可能发生的两个最复杂的问题:死锁和违反约束。
翻译自: https://www.ibm.com/developerworks/java/library/j-dclp3/index.html