----Android培训 、 Java培训 、期待与您交流! ----
一.类加载器基本概念
1.1 类加载器
Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到的内存空间中。
1.2 类加载器体系
引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。BootStrap的加载路径为JRE/lib/rt.jar。
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。ExtClassLoader加载目录为JRE/lib/ext/*.jar。
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类,通常Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。AppClassLoader加载目录为CLASSPATH指定的所有jar或目录。
用户自定义类加载器:该类继承ClassLoader,通常自定义加载器重写父类的findClass()方法,然后调用loadClass()方法,一般来说很少重写父类的loadClass()方法,因为该方法实现了委托代理机制,如果你不想使用委托代理机制,那么你可以实现自己的加载的算法,建议:最好不要覆盖委托代理机制因为委托代理机制。
1.3 Java 虚拟机是如何判定两个 Java 类是相同的
Java 虚拟机首先要看类的完全限定名是否相同,其次看加载此类的类加载器是否一样。当只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。
1.4 委托代理机制
初始加载器:启动某个类的加载器,对用户自定义的类加载器而言,就是调用loadClass方法的那个类加载器。由于有委托机制的存在,实际上是由它的某个父类加载器对字节码进行加载的。
类的定义加载器:真正加载字节码的那个类加载器。
委托代理:步骤1:类加载器在调用加载方法的时候会尝试查找是否加载过该份字节码,如果加载过,则返回在字节码的Class 对象。步骤2:如果初始加载器没有加载过,则会让父类加载器调用它的加载方法,尝试查找是否加载过该份字节码,如果加载过,则返回在字节码的Class 对象。(该步骤可能有很多)步骤3:如果所有的类加载器都没有加载过该份字节码,那么就迭代返回加载该份字节码,首先从初始加载器的顶层父类加载器(引导类加载器),去尝在它的加载目录中去尝试加载该份字节码,如果加载成功,则会返回该字节码的Class对象,否则,然后从初始加载器的父类加载器(扩展类加载器),去尝在它的加载目录中去尝试加载该份字节码,如果加载成功,则会返回该字节码的Class对象,否则 首先从初始加载器的顶层父类加载器(引导类加载器),去尝在它的加载目录中去尝试加载该份字节码,如果加载成功,则会返回该字节码的Class对象,否则,其次 从初始加载器的父类加载器(系统类加载器),去尝在它的加载目录中去尝试加载该份字节码,如果加载成功,则会返回该字节码的Class对象,否则,只有让该初始加载器去加载,如果加载成功,则返回该字节码的Class对象,否则最后只有抛出 ClassNotFoundException。
加载类的过程:前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass()来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loade)。在 Java 虚拟机判断两个类是否相同的时候,用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类com.example.Outter引用了类com.example.Inner,则由类com.example.Outter的定义加载器负责启动类com.example.Inner的加载过程。方法loadClass()抛出的是Java.lang.ClassNotFoundException异常;方法defineClass抛出的是Java.lang.NoClassDefoundError异常。类加载器在成功加载某个类之后,会把得到的Java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即loadClass方法不会被重复调用。
委托代理的优点:为了防止你随意编写与Java核心库中的类的完全限定名相同,造成程序的混乱。例如,如果有人想编写一个与Java核心库相同的名称(Java.lang.String),由于它采用的是委托模式类加载器的加载器的算法,它会委托给自己的父类,最终它会委托给引导类加载器,因为Java的核心库的类由引导类加载器加载,引导类加载器会在它的类搜索路径中搜索,在该路径中发现了Java.lang.String,并加载该类,而你写的类由于与,Java,lang.String同名,所以你写得类没有被加载,而加载的是Java的核心库里面的类。
1.5 Java.lang.ClassLoader类介绍
方法 | 说明 |
---|---|
getParent() | 返回该类加载器的父类加载器。 |
loadClass(String name) | 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。 |
findClass(String name) | 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。 |
findLoadedClass(String name) | 查找名称为 name的已经被加载过的类,返回的结果是Java.lang.Class类实例 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。 这个方法被声明为 final的。 |
resolveClass(Class<?> c) | 链接指定的 Java 类。 |
二.类的加载方式详解
2.1 加载的文件来源
网络上的.JAR文件,压缩文件,.class文件,本地 .class /.Jar文件等Java可以识别的文件。
2.2 类加载器加载
用户自定义类加载器加载:(在没有覆盖改变loadClass方法的委托机制加载算法的时候,切记不要将字节码文件放在ClassPath路径下,在重写findClass方法的时候,重新改变字节码的路径搜索,在Eclipse中,字节码一般放在bin目录及其子目录下,切记不要放在该目录下),通过loadClass方法加载类。
JDK自带类加载器加载:系统类加载器加载ClassPath目录下的字节码文件。扩展类加载器加载RE/lib/ext目录下的字节码文件。引导类加载器加载JRE/lib/rt.jar目录下的字节码文件。
线程上下文类加载器:线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源.
2.3 Class.forName
Class.forName()是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name,boolean initialize,ClassLoader loader) 和 Class.forName()(String className).第一种形式的参数 name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。第二种形式则相当于设置了参数 initialize的值为 true,loader的值为当前类的类加载器.Class.forName()的一个很常见的用法是在加载数据库驱动的时候。如Class.forName()("com.mysql.Driver").newInstance()用来加载 MySQL 数据库的驱动。
2.4 两种方式的比
类加载器的加载没有初始化,而Class.forName方法则会可以选择初始化,或不初始化,可以灵活选择,在延迟初始化时可以让initialize为false,反之为true,而类加载器则无法做到这点。
三.类加载器的源代码
3.1 loadClass方法的源码
protected Class loadClass(String name, boolean resolve)throws ClassNotFoundException{
Class c = findLoadedClass(name);// 类加载器在调用加载方法的时候会尝试查找是否加载过该份字节码,如果加载过,则返回在字节码的Class 对象。
if(c ==null){
long t0 = System.nanoTime();
try{if (parent!=null){c=parent.loadClass(name,false);//委托模式的代码实现
}else{c = findBootstrapClassOrNull(name);//当委托到BootStrap类加载器时
}}catch (ClassNotFoundException e){}
if (c ==null) {//如果初始类加载器的父加载器都无法加载的话就由该初始加载器自己加载,如果初始加载器无法加载就抛出ClassNotFoundException异常。
long t1 = System.nanoTime();
c = findClass(name);//用户自定义类加载器就是重写该方法,重新定义自己的字节码搜索路径
if (resolve){//是否初始化
resolveClass(c);}//解析的具体实现,该方法的最初始的方法是用C++代码实现的
return c;}
3.2 findClass方法的源码
protected Class findClass(String name)throws ClassNotFoundException
{
throw new ClassNotFoundException(name);
}//该方法一般是给用户自定义类加载器重写的。如果JDK自带类加载器加载的话,调用该方法就会抛出ClassNotFoundException异常,也就是JDK自带的类加载器无法加载类的时候。
3.3 defineClass方法的源码
private native Class defineClass1(String name, byte[] b, int off, int len, ProtectionDomain pd, String source)//类的类加载核心方法,主要对二进制的字节码文件加载,该方法主要对二进制的字节码文件加载,该方法是本地方法,由C++代码实现。
protected final Class defineClass(byte[] b, int off, int len)throws ClassFormatError//该方法被废弃了,但还是可以用 {return defineClass(null, b, off, len, null);}
protected final Class defineClass(String name, byte[] b, int off, int len) throws ClassFormatError//该方法是主流的,不过它 要指定你的类的加载的二进制 字节码的完全限定名。
{return defineClass(name, b, off, len, null);}
protected final Class defineClass(String name, byte[] b, int off, int len,ProtectionDomain protectionDomain)throws ClassFormatError
{Class c = defineClass1(name, b, off, len, protectionDomain, source);
return c;}
四.类加载器的安全
4.1 类加载器体系安全
- 保护善意代码不受恶意代码的干扰
- 保护已验证的类库
- 代码放入有不同的行为限制的各个保护域中
- 类加载体系通过使用不同的类加载器把类放入不同的名字空间中从而保护善意代码不受恶意代码的干扰。
- JVM为每个类加载器维护一个名字空间。
4.2 通过类加载器对字节码文件加密举例
//测试类:
public class Exam {
public void print()
{
System.out.println(12345);
}
}
//加密算法类:
public class Encryption{
private String srcDir;
private String destDir;
public Encryption(String srcDir,String destDir)
{
this.srcDir=srcDir;
this.destDir=destDir;
}
public void encryption(String name) throws IOException
{
String filepath=null;
InputStream input=null;
int data;
File f0=new File(destDir+"\\"+name.substring(0, name.lastIndexOf('.')).replace('.', '\\'));
if(!f0.exists()) f0.mkdirs();
File f1=new File(f0.getAbsolutePath()+name.substring(name.lastIndexOf('.'), name.length()).replace('.', '\\')+".class");
FileOutputStream out=new FileOutputStream(f1);
if(!f1.exists()) f1.createNewFile();
filepath=srcDir+"\\"+name.replace(".","\\")+".class";
File file=new File(filepath);
URL url=file.toURI().toURL();
input=url.openStream();
while((data=input.read())!=-1)
{
out.write(data^0xff);
}
out.close();
}
}
//对测试类加密主类:
public class Test{
public static void main(String[] args) throws IOException
{
String name="loadertest.Exam";
Encryption encryption=new Encryption(args[0], args[1]);
encryption.encryption(name); }}
//解密类加载器:
class APPClassLoaderTest extends ClassLoader
{
private String dirpath=null;
public APPClassLoaderTest(String dirpath)
{
this.dirpath=dirpath;
}
@Override
protected Class findClass(String name)
{
Classcls=null;
byte[] b=null;
try { b = this.getOutputStream(name).toByteArray();} catch (IOException e) {
e.printStackTrace();}cls=defineClass(name,b, 0, b.length); return cls;}
public ByteArrayOutputStream getOutputStream(String name) throws IOException
{String filepath=null;InputStream input=null;int data=-1;
filepath=dirpath+"\\"+name.replace(".","\\")+".class";
File file=new File(filepath);
URL url=file.toURI().toURL();
input=url.openStream();ByteArrayOutputStream bo=new ByteArrayOutputStream();
while((data=input.read())!=-1)
{bo.write(data^0xff);} return bo;}}
//总测试类:
public class TestClass {
@SuppressWarnings("unchecked")
public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
Class clazz= new APPClassLoaderTest(args[1]).loadClass("loadertest.Exam");
Object obj=clazz.newInstance();
clazz.getMethod("print").invoke(obj);
System.out.println(TestClass.class.getClassLoader());
System.out.println(obj.getClass().getClassLoader());}}
五.类加载器的举例
5.1 一般继承类加载器
public class MyClassLoader extends ClassLoader {
@SuppressWarnings("deprecation")
@Override
public Class findClass(String name)
{
Classclazz=null;
try{
String dir=System.getProperty("user.dir");
String path=dir+File.separator+"src"+File.separator;
String fp=path+name.replace(".",File.separator)+".class";
@SuppressWarnings("resource")
FileInputStream fin=new FileInputStream(fp);
ByteArrayOutputStream bou=new ByteArrayOutputStream();
int bit;
while((bit=fin.read())!=-1)
{
bou.write(bit);
}
byte[] b=bou.toByteArray();
clazz=defineClass(b,0,b.length);
return clazz;
}
catch(Exception e){}
return clazz;}
}
5.2 测试不同的类加载器对象加载同一份字节码的Class对象是否相同
public class Test {
public static void main(String[] args) throws Exception {
MyClassLoader my0=new MyClassLoader();
MyClassLoader my1=new MyClassLoader();
Class cls0=my0.loadClass("com.Exam");
Class cls1=my1.loadClass("com.Exam");
System.out.println(cls0.getClassLoader());
System.out.println(cls1.getClassLoader());
System.out.println(cls0==cls1); } }
//测试结果:
com.MyClassLoader@106d69c
com.MyClassLoader@25154f
false
六.类加载器与 Web 容器
对于运行在 Java EE™容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。
七.类加载时的异常错误分析
7.1 ClassNotFoundException
ClassNotFoundException 是最常见的类装入异常类型。它发生在装入阶段。Java 规范对 ClassNotFoundException 的
描述是这样的:当应用程序试图通过类的字符串名称,使用以下三种方法装入类,但却找不到指定名称的类定义时抛出该异常。
-
类 Class 中的 forName() 方法。
-
类 ClassLoader 中的 findSystemClass() 方法。
-
类 ClassLoader 中的 loadClass() 方法。
所以,如果显式地装入类的尝试失败,那么就抛出 ClassNotFoundException。
异常测试:ClassNotFoundExceptionTest.java
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)
//因为这个测试试图使用对loadClass()的显式调用来进行装入,所以抛出 ClassNotFoundException。通过抛出ClassNotFoundException,类装入器提示,定义类时所需要的字节码在类装入器所查找的位置上不存在。这些异常修复起来通常比较简单。可以用 JDK 的verbose选项检查类路径,如果类路径设置正确,但是仍然看到这个错误,那么就是需要的类在类路径中不存在。要修复这个问题,可以把类移动到类路径中指定的目录或JAR文件中,或者把类所在的位置添加到类路径中。
7.2 NoClassDefFoundError
NoClassDefFoundError 是类装入器在装入阶段抛出的另一个常见异常。JVM 规范对 NoClassDefFoundError的定义如下:如果 Java 虚拟机或 ClassLoader 实例试图装入类定义(作为正常的方法调用的一部分,或者作为使用 new 表达式创建新实例的一部分),但却没有找到类定义时抛出该异常。 当目前执行的类已经编译,但是找不到它的定义时,会存在 searched-for 类定义。
异常测试:NoClassDefFoundErrorTest.java
public class NoClassDefFoundErrorTest {
public static void main(String[] args) {
A a = new A();}}
public class A extends B {}
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时抛出。
7.3 ClassCastException
类装入器能够抛出的另一个异常是ClassCastException。它是在类型比较中发现不兼容类型的时候抛出的。JVM 规范指定ClassCastException是:该异常的抛出,表明代码企图把对象的类型转换成一个子类,而该对象并不是这个子类的实例。
异常测试: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, newString("abc"));} catch (ClassCastException e){ e.printStackTrace();}}}
//异常测试中,调用了 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。当检查对象(例如异常测试中的item)并把类型转换成目标类(Integer)时,类装入器会检查以下规则:对于普通对象(非数组):对象必须是目标类的实例或目标类的子类的实例。如果目标类是接口,那么会把它当作实现了该接口的一个子类。对于数组类型:目标类必须是数组类型或 java.lang.Object、java.lang.Cloneable 或 java.io.Serializable。如果违反了以上任何一条规则,那么类装入器就会抛出 ClassCastException。修复这类异常的最简单方式就是仔细检查对象要转换到的类型是否符合以上提到的规则。在某些情况下,在做类型转换之前用 instanceof 进行检查是有意义的。
7.4 UnsatisfiedLinkError
把本机调用;链接接到对应的本机时,类装入器扮演着重要角色。如果程序试图装入一个不存在或者放错的本机库时,在链接阶段的解析过程会发生UnsatisfiedLinkError,JVM规范规定UnsatisfiedLinkError:对于声明为 native 的方法,如果 Java 虚拟机找不到和它对应的本机语言定义,就会抛出该异常。当调用本机方法时,类装入器会尝试装入定义了该方法的本机库。如果找不到这个库,就会抛出这个错误。
异常测试: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 中定义的。因为这个库不存在,所以在程序运行时会发生以下错误:
Thejava class could not be loaded. java.lang.UnsatisfiedLinkError: Can't find library myNativeLibrary (myNativeLibrary.dll) insun.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.(UnsatisfiedLinkErrorTest.java:6)
//本机库的装入由调用 System.loadLibrary() 方法的类的类装入器启动,在异常测试中,就是UnsatisfiedLinkErrorTest的类装入器。根据使用的类装入器,会搜索不同的位置:对于由 bootstrap 类装入器装入的类,搜索 sun.boot.library.path。对于由扩展类装入器装入的类,先搜索 java.ext.dirs,然后是 sun.boot.library.path,然后是 java.library.path。对于由系统类装入器装入的类,搜索 sun.boot.library.path,然后是 java.library.path。在异常测试中,UnsatisfiedLinkErrorTest 类是由系统类装入器装入的。要装入所引用的本机库,这个类装入器先查找sun.boot.library.path,然后查找 java.library.path。因为在两个位置中都没有需要的库,所以类装入器抛出UnsatisfiedLinkageError。一旦理解了库装入过程所涉及的类装入器,就可以通过把库放在合适位置来解决这类问题。
7.5 ClassCircularityError
JVM 规范指定 ClassCircularityError 的抛出条件是:类或接口由于是自己的超类或超接口而不能被装入。这个错误是在链接阶段的解析过程中抛出的。这个错误有点奇怪,因为 Java 编译器不允许发生这种循环情况。但是,如果独立地编译类,然后再把它们放在一起,就可能发生这个错误。
7.6 ClassFormatError
JVM 规范指出,抛出 ClassFormatError 的条件是:负责指定所请求的编译类或接口的二进制数据形式有误。这个异常是在类装入的链接阶段的校验过程中抛出。如果字节码发生了更改,例如主版本号或次版本号发生了更改,那么二进制数据的形式就会有误。例如,如果对字节码故意做了更改,或者在通过网络传送类文件时现出了错误,那么就可能发生这个异常。修复这个问题的惟一方法就是获得字节码的正确副本,可能需要重新进行编译。
7.7 ExceptionInInitializerError
根据 JVM 规范,抛出 ExceptionInInitializer 的情况是:如果初始化器突然完成,抛出一些异常 E,而且 E 的类不是 Error 或者它的某个子类,那么就会创建 ExceptionInInitializerError 类的一个新实例,并用 E 作为参数,用这个实例代替 E。如果 Java 虚拟机试图创建类 ExceptionInInitializerError 的新实例,但是因为出现 Out-Of-Memory-Error 而无法创建新实例,那么就抛出 OutOfMemoryError 对象作为代替。
异常测试:ExceptionInInitializerErrorTest.java
public class ExceptionInInitializerErrorTest {
public static void main(String[] args) {
A a = new A();}}class A {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.(ExceptionInInitializerErrorTest.java:12)
... 1 more
//这个错误在类装入的初始化阶段抛出。修复这个错误的方法是检查造成 ExceptionInInitializerError 的异常并寻找阻止抛出这个异常的方式。