本文(共四部分),是第二篇,着眼于运行应用程序时通常引发的各种类加载异常。 这些异常虽然很常见,但Java开发人员通常并不十分了解。 本文依次列举了每种异常,提供了详细的示例,以突出它们的行为,解释其原因并显示一些可能的解决方法。 本文从非常常见的ClassNotFoundException
,并介绍了鲜为人知的异常,例如ExceptionInInitializerError
。
在开始本文之前,您应该熟悉类加载器委托模型,以及类链接的阶段和阶段。 我们建议您先阅读本系列的第一篇文章 。
ClassNotFoundException
ClassNotFoundException
是最常见的类加载异常类型。 它发生在加载阶段。 Java规范对ClassNotFoundException
描述如下:
当应用程序尝试使用其字符串名称通过其字符串名称加载类时抛出:但找不到具有指定名称的类的定义。
- 类
Class
的forName()
方法。- 类
ClassLoader
的findSystemClass method()
。- 类
ClassLoader
的loadClass()
方法。
因此,如果显式尝试加载类失败,则抛出ClassNotFoundException
。 清单1中的测试用例提供了引发ClassNotFoundException
示例代码:
清单1. ClassNotFoundExceptionTest.java
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassNotFoundExceptionTest {
public static void main(String args[]) {
try {
URLClassLoader loader = new URLClassLoader(new URL[] { new URL(
"file://C:/CL_Article/ClassNotFoundException/")});
loader.loadClass("DoesNotExist");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
该测试用例定义了一个类加载器( MyClassLoader
),该类加载器用于加载不存在的类( DoesNotExist
)。 运行它时,发生以下异常:
java.lang.ClassNotFoundException: DoesNotExist
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 ClassNotFoundExceptionTest.main(ClassNotFoundExceptionTest.java:11)
抛出ClassNotFoundException
是因为测试使用显式调用loadClass()
尝试加载。
通过ClassNotFoundException
,类加载器会通知您,定义类所需的字节码根本不在类加载器所要查找的位置。 这些异常通常很容易解决。 您可以通过使用IBM verbose选项进行检查来确保所使用的类路径已按预期设置。 (有关此选项的更多信息,请参阅本系列的第一篇文章 。)如果正确设置了类路径,但是您仍然看到错误,则所需的类不在类路径上。 要解决此问题,请将类移到类路径中指定的目录或JAR文件中,或者将存储类的位置添加到类路径中。
NoClassDefFoundError
NoClassDefFoundError
是类加载器在加载阶段引发的另一个常见异常。 JVM规范如下定义NoClassDefFoundError
:
如果Java虚拟机或ClassLoader
实例尝试加载类的定义(作为常规方法调用的一部分或使用新表达式创建新实例的一部分)而抛出,则ClassLoader
的定义。
当前正在编译的类在编译时就存在搜索到的类定义,但是无法再找到该定义。
本质上,这意味着由于隐式类加载失败而引发了NoClassDefFoundError
。
清单2至清单4中的测试用例产生了NoClassDefFoundError
因为类B
的隐式加载将失败:
清单2. NoClassDefFoundErrorTest.java
public class NoClassDefFoundErrorTest {
public static void main(String[] args) {
A a = new A();
}
}
清单3. A.java
public class A extends B {
}
清单4. B.java
public class B {
}
在这些清单中编译了代码之后,请删除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:504)
at NoClassDefFoundErrorTest.main(NoClassDefFoundErrorTest.java:3)
类A
延伸类B
; 因此,在加载类A
时,类加载器会隐式加载类B
因为类B
不存在,所以将引发NoClassDefFoundError
。 如果已明确告知类加载器加载类B
(例如,通过loadClass("B")
调用),则将抛出ClassNotFoundException
。
显然,要解决此特定示例中的问题,必须在合适的类加载器的类路径上存在类B
这个例子似乎微不足道,不切实际。 但是,在具有许多类的复杂的,真实世界的系统中,当在打包或部署期间丢失类时,可能会发生这种情况。
在此示例中, A
扩展了B
; 但是,如果A
以任何其他方式引用B
(例如,作为方法参数或作为实例字段),仍然会发生相同的错误。 如果两个类之间的关系是引用关系而不是继承关系,那么将在第一次主动使用A
而不是在加载A
期间抛出错误。
ClassCastException
类加载器可能引发的另一个异常是ClassCastException
。 它是由于在类型比较中发现不兼容的类型而引发的。 JVM规范说ClassCastException
是:
抛出该异常以指示代码已尝试将对象强制转换为不是实例的子类。
清单5展示了引发ClassCastException
的代码示例:
清单5. ClassCastException.java
public class ClassCastExceptionTest {
public ClassCastExceptionTest() {
}
private static void storeItem(Integer[] a, int i, Object item) {
a[i] = (Integer) item;
}
public static void main(String args[]) {
Integer[] a = new Integer[3];
try {
storeItem(a, 2, new String("abc"));
} catch (ClassCastException e) {
e.printStackTrace();
}
}
}
在清单5中,将storeItem()
方法,并传入一个Integer
数组,一个int
和一个字符串。 但是,在内部,该方法有两件事:
- 它将
String
对象类型隐式转换为Object
类型(用于参数列表)。 - 它将此
Object
类型显式转换为Integer
类型(在方法定义中)。
运行该程序时,会发生以下异常:
java.lang.ClassCastException: java.lang.String
at ClassCastExceptionTest.storeItem(ClassCastExceptionTest.java:6)
at ClassCastExceptionTest.main(ClassCastExceptionTest.java:12)
由于测试用例试图将String
类型的内容转换为Integer
,因此显式强制转换抛出异常。
给定一个要测试的对象(例如清单5中的item
)和一个目标类( Integer
)被强制转换为对象,类加载器将检查以下规则:
- 对于普通对象(非数组):对象必须是目标类的实例或目标类的子类。 如果目标类是接口,则如果实现该接口,则将其视为子类。
- 对于数组类型:目标类必须是数组类型或
java.lang.Object
,java.lang.Cloneable
或java.io.Serializable
。
如果违反了以上任何一个规则,则类加载器将ClassCastException
。 解决此类异常的最简单方法是仔细检查对象所投射到的类型是否符合上述规则。 在某些情况下,在进行类强制转换之前使用instanceof
check可能是明智的。
UnsatisfiedLinkError
类加载器在将本机调用链接到其适当的本机定义方面起着重要作用。 当程序尝试加载不存在或放错位置的本机库时,在链接阶段的解析阶段会发生UnsatisfiedLinkError
。 JVM规范说UnsatisfiedLinkError
是:
如果Java虚拟机无法找到声明为native
的方法的适当本地语言定义,则抛出该异常。
调用本机方法时,类加载器将尝试加载定义该方法的本机库。 如果找不到此库,则会引发错误。
清单6展示了一个抛出UnsatisfiedLinkError
的测试用例:
清单6. UnsatisfiedLinkError.java
public class UnsatisfiedLinkErrorTest {
public native void call_A_Native_Method();
static {
System.loadLibrary("myNativeLibrary");
}
public static void main(String[] args) {
new UnsatisfiedLinkErrorTest().call_A_Native_Method();
}
}
此代码调用本机方法call_A_Native_Method()
这是在本机库中定义myNativeLibrary
。 因为此库不存在,所以在运行程序时发生以下错误:
The java class could not be loaded. java.lang.UnsatisfiedLinkError:
Can't find library myNativeLibrary (myNativeLibrary.dll)
in sun.boot.library.path or java.library.path
sun.boot.library.path=D:\sdk\jre\bin
java.library.path= D:\sdk\jre\bin
at java.lang.ClassLoader$NativeLibrary.load(Native Method)
at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:2147)
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:2006)
at java.lang.Runtime.loadLibrary0(Runtime.java:824)
at java.lang.System.loadLibrary(System.java:908)
at UnsatisfiedLinkErrorTest.<clinit>(UnsatisfiedLinkErrorTest.java:6)
本地库的加载由调用System.loadLibrary()
的类的类加载器(清单6中的UnsatisfiedLinkErrorTest
的类加载器System.loadLibrary()
启动。根据这是什么类加载器,搜索不同的位置:
- 对于由引导类加载器加载的类,将搜索
sun.boot.library.path
。 - 对于由扩展类加载器加载的类,首先搜索
java.ext.dirs
,然后搜索sun.boot.library.path
,然后搜索java.library.path
。 - 对于由系统类加载器加载的类,将搜索
sun.boot.library.path
,然后是java.library.path
。
在清单6中, UnsatisfiedLinkErrorTest
类由系统类加载器加载。 要加载引用的本机库,此类加载器将在sun.boot.library.path
查找,然后在java.library.path
查找。 由于该库在这两个位置均不可用,因此类加载器会抛出UnsatisfiedLinkageError
。
一旦了解了库加载中涉及的类加载器,就可以通过将库放置在适当的位置来解决这些类型的问题。
ClassCircularityError
JVM规范说,如果发生ClassCircularityError
情况,将引发ClassCircularityError
:
无法加载类或接口,因为它将是其自己的超类或超接口。
在链接阶段的解决阶段会引发此错误。 这是一个有点奇怪的错误,因为Java编译器不允许出现这种循环情况。 但是,如果您要分别编译类,然后将它们组合在一起,则可能会发生错误。 想象以下情况。 首先,编译清单7和清单8中的类:
清单7. A.java
public class A extends B {
}
清单8. B.java
public class B {
}
然后,分别编译清单9和清单10中的类:
清单9. A.java
public class A {
}
清单10. B.java
public class B extends A {
}
最后,以类A
从清单7和类B
从清单10和运行的应用程序试图加载或者A
或B
。 这似乎是不太可能发生的情况,但是在将许多不同部分组合在一起的复杂系统中,可能会发生非常类似的事情。
显然,要解决此问题,必须避免循环类层次结构。
ClassFormatError
JVM规范指出,如果发生以下情况,将引发ClassFormatError
:
旨在指定请求的已编译类或接口的二进制数据格式错误。
在类加载的链接阶段的验证阶段抛出此异常。 如果字节码已更改,则二进制数据可能会格式错误-例如,如果更改了主编号或次编号。 例如,如果字节码已被人为入侵,或者通过网络传输类文件时发生错误,则可能会发生这种情况。
解决此问题的唯一方法是可能通过重新编译来获取字节码的正确副本。
ExceptionInInitializerError
根据JVM规范,抛出ExceptionInInitializer
:
- 如果初始化通过抛出一些异常突然完成
E
,和如果类的E
没有Error
或它的一个子类,那么这个类的一个新实例ExceptionInInitializerError
,与E
作为参数,创建并用来代替E
。- 如果Java虚拟机尝试创建
ExceptionInInitializerError
类的新实例,但由于发生Out-Of-Memory-Error
而无法创建新实例,则OutOfMemoryError
对象。
清单8中的代码抛出ExceptionInInitializerError
:
清单8. ExceptionInInitializerErrorTest.java
public class ExceptionInInitializerErrorTest {
public static void main(String[] args) {
A a = new A();
}
}
class A {
// If the SecurityManager is not turned on, a
// java.lang.ExceptionInInitializerError will be thrown
static {
if(System.getSecurityManager() == null)
throw new SecurityException();
}
}
当静态代码块中发生异常时,将自动捕获并包装一个ExceptionInInitializerError
。 在下面的输出中可以看到:
Exception in thread "main" java.lang.ExceptionInInitializerError
at ExceptionInInitializerErrorTest.main(ExceptionInInitializerErrorTest.java:3)
Caused by: java.lang.SecurityException
at A.<clinit>(ExceptionInInitializerErrorTest.java:12)
... 1 more
在类加载的初始化阶段引发此错误。 解决该问题的方法是检查引起ExceptionInInitializerError
(在堆栈跟踪中显示在Caused by:
),并找到一种阻止引发该异常的方法。
下一步是什么
在本文中,您了解了各种类加载异常,从最基本的错误到一些更神秘的错误。 在本系列的下一篇文章中,我们将介绍在运行更复杂的应用程序时可能遇到的其他一些类加载问题。
翻译自: https://www.ibm.com/developerworks/java/library/j-dclp2/index.html