JVM中的类都必须经历过完整的加载、连接和初始化3个类加载步骤。经历过这3个步骤之后,它就可以随时随地被使用了,开发者可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。当一个类被加载进JVM中开始算起,直至最终被卸载出内存,它的整个生命周期也就随之结束。
类加载器
类加载器的主要任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM内部,然后转换为一个与目标类对应的java.lang.Class对象实例。因为类加载器并没有绑定在JVM内部,能够更加灵活和动态地执行类加载操作,所以在OSGi、字节码加解密领域大放异彩。
Java开发者经常遇见java.lang.ClassNotFoundException异常或java.lang.NoClassDefFoundError异常,或许手脚无措,但了解类加载器后,一旦出现异常可快速地根据错误异常日志定位问题和解决问题。除此之外,在一些特殊的应用场景中,比如需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时,就需要与类加载器打交道了,也就是说,开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。在此大家需要注意,从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:
- Bootstrap ClassLoader
Bootstrap ClassLoader也称之为启动类加载器,它由C++语言编写并嵌套在JVM内部,主要负责加载“JAVA_HOME/lib”目录中的所有类型,或者由选项“-Xbootclasspath”指定路径中的所有类型。 -
ExtClassLoader
ExtClassLoader采用Java语言进行编写的,前者主要负责加载“JAVA_HOME/lib/ext”扩展目录中的所有类型。 - AppClassLoader
AppClassLoader派生于ClassLoader,同时也是ExtClassLoader的子类,主要负责加载ClassPath目录中的所有类型。
/**
* 不同类加载器的加载类型
*/
public class ClassLoaderTest {
public static void main(String[] args) {
/* Bootstrap ClassLoader负责加载JAVA_HOME\lib目录中的类型 */
ClassLoader loader = System.class.getClassLoader();
System.out.println(null != loader ? loader.getClass().getName() : null);
/* ExtClassLoader负责加载JAVA_HOME\lib\ext目录中的类型 */
System.out.println(CollationData_ar.class.getClassLoader().getClass().getName());
/* AppClassLoader负责加载ClassPath目录中的类型 */
System.out.println(ClassLoaderTest.class.getClassLoader().getClass().getName());
}
}
程序输出如下:
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
上述代码示例中,通过getClassLoader()方法成功获取了目标类的类加载器,返回值就是一个ClassLoader的派生实例,但这个方法却并非属于ClassLoader,而属于java.lang.Class。java.lang.System类包含在“JAVA_HOME/lib”目录中,由Bootstrap ClassLoader负责加载,但是程序最终输出的类加载器名称却是Null,当然这并不代表类加载器不存在,而是因为启动类加载器本身是由C++语言编写并嵌套在JVM内部的,所以输出结果才会为Null。sun.text.resources.CollationData_ar类包含在“JAVA_HOME/lib/ext”扩展目录中,由ExtClassLoader负责加载。而ClassLoaderTest类是自己编写的一个Java类,包含在ClassPath目录中,因此由AppClassLoader负责加载。
抽象类ClassLoader
如果当前的类加载器无法满足我们的需求时,便可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。在程序中编写一个自定义类加载器只需要继承抽象类ClassLoader并重写其findClass()方法即可。当编写好自定义类加载器后,便可以在程序中调用loadClass()方法来实现类加载操作。
ClassLoader的常用方法
按照双亲委派模型的规则,除了启动类加载器之外,程序中每一个类加载器都应该拥有一个超类加载器,比如AppClassLoader的超类加载器就是ExtClassLoader。如下所示:
使用getParent()方法获取当前类加载器的超类加载器
/**
* 获取当前类加载器的超类加载器
*/
public class GetSuperClassLoader {
public static void main(String[] args) {
/* 获取目标类的类加载器 */
ClassLoader classLoader = GetSuperClassLoader.class.getClassLoader();
System.out.println("当前类加载器->" + classLoader.getClass().getName());
/* 获取超类加载器 */
classLoader = classLoader.getParent();
System.out.println("当前类加载器的超类加载器->" + classLoader.getClass().getName());
}
}
程序输出如下:
当前类加载器->sun.misc.Launcher$AppClassLoader
当前类加载器的超类加载器->sun.misc.Launcher$ExtClassLoader
在程序中,一个普通的Java类一旦继承抽象类ClassLoader,并重写其findClass()方法后,它就已经是一个自定义类加载器了。一般情况下,findClass()和defineClass()通常是一起组合使用的。在重写findClass()方法时,可以指定相应的逻辑处理操作,比如,如果一个字节码文件在编译的时候进行了加密处理,以防止其他人进行反编译,那么这个特殊的字节码文件在被加载进JVM内部之前,首先需要做的就是执行解密操作,当成功解密之后,就可以调用defineClass()方法将解密后的byte数组转换为一个类的Class对象实例。
使用defineClass()读取字节码的数据源不仅限于磁盘,还可以是网络,总之只要是能够成功获取一个类的字节码,就都能通过defineClass()方法将其转换为一个类的Class对象实例。在此大家需要注意,如果希望在类被加载到JVM内部时就被链接(Link),那么便可以调用resolveClass()方法,当然也可以由JVM来选择应该在什么时候才执行链接操作。
双亲委派模型
除了启动类加载器之外,程序中每一个类加载器都应该拥有一个超类加载器,比如AppClassLoader的超类加载器就是ExtClassLoader,而开发人员自己编写的自定义类加载器的超类就是AppClassLoader。那么当一个类加载器接收到一个类加载任务的时候,它并不会立即展开加载,而是将加载任务委派给它的超类加载器去执行,每一层的类加载器都采用相同的方式,直至委派给最顶层的启动类加载器为止。如果超类加载器无法加载委派给它的类时,便会将类的加载任务退回给它的下一级类加载器去执行加载。
使用双亲委派模型的优点就是能够有效地确保一个类的全局唯一性,当程序中出现多个全限定名相同的类时(比如都叫做com.test.User),类加载器在执行加载的时候,始终都只会加载其中的某一个类,不会2个类都执行加载,如果想通过defindClass()方法进行显示的加载,JVM将会抛出异常。
双亲委派模型的具体实现
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
/* 首先检查目标类型之前是否已经被成功加载过了 */
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
/* 如果存在超类加载器,就委派给超类加载器执行加载 */
if (parent != null) {
c = parent.loadClass(name, false);
} else {
/* 如果不存在超类加载器,就直接委派给顶层的启动类加载器执行加载 */
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
/* 当抛出ClassNotFoundException 异常时,则意味着超类加载器加载失败 */
}
if (c == null) {
/* 如果超类加载器无法加载时,则自行加载 */
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
上述代码示例中,首先会由findLoadedClass()方法检查目标类型之前是否已经被成功加载过了,如果确定没有被加载过,则调用超类加载器的loadClass()方法,将类的加载任务委派给它的超类加载器,如果不存在超类加载器,就通过findBootstrapClassOrNull()方法调用本地方法findBootstrapClass()直接委派给顶层的启动类加载器去执行加载。如果超类加载器抛出ClassNotFoundException异常时,则意味着超类加载器无法加载目标类型,那么则只能够调用自身的findClass()方法执行类加载操作。
在此大家需要注意,由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式而已。比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。
自定义类加载器
如果当前的类加载器无法满足我们的需求时,就需要在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。比如当一个字节码文件在编译的时候如果进行了加密处理,那么在被类加载器执行类加载操作时,首先要做的事情就是解密处理,否则类加载器就会认为这个字节码并不符合JVM规范,它不是一个有效的字节码文件。其次就是程序中如果没有显式地指定类加载器时,AppClassLoader就是任务委派的发起者。AppClassLoader主要负责加载ClassPath目录中的所有类型,但是如果被加载的类型并没有包含在ClassPath目录中时,程序最终就会抛出java.lang.ClassNotFoundException异常。为了满足这些特殊的应用场景,开发人员就需要在程序中编写自定义类加载器。
想要在程序中实现一个自定义类加载器,只需要继承抽象类ClassLoader,并重写其findClass()方法即可。在此大家需要注意,尽管Java虚拟机规范将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器,但是从严格意义上来说,由Java开发人员编写的自定义类加载其实并不属于Java体系结构的组成部分,实际上它仅仅只是属于Java运行时程序的一部分而已。
编写自定义类加载器
/**
* 自定义类加载器
*/
public class MyClassLoader extends ClassLoader {
private String byteCode_Path;
public MyClassLoader(String byteCode_Path) {
this.byteCode_Path = byteCode_Path;
}
@Override
protected Class<?> findClass(String className)
throws ClassNotFoundException {
byte value[] = null;
BufferedInputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream
(byteCode_Path + className + ".class"));
value = new byte[in.available()];
in.read(value);
} catch (IOException e) {
e.printStackTrace();
} finally {
/* 释放资源 */
if (null != in) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/* 将byte数组转换为一个类的Class对象实例 */
return defineClass(value, 0, value.length);
}
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("D:/");
System.out.println("加载目标类的类加载器->"
+ classLoader.loadClass("Test").getClassLoader().getClass().getName());
System.out.println("当前类加载器的超类加载器->"
+ classLoader.getParent().getClass().getName());
}
}
程序输出如下:
加载目标类的类加载器->MyClassLoader
当前类加载器的超类加载器->sun.misc.Launcher$AppClassLoader
上述代码示例中,演示了如何编写一个自定义类加载器来加载指定目录中的类型,由于目标类型Test并非包含在ClassPath目录中,自然也就无法被AppClassLoader执行加载。在程序中指定了加载目标类型的类加载器是MyClassLoader,那么它就是任务委派的发起者,MyClassLoader会按照双亲委派模型将Test类的加载任务委派给超类加载器,而AppClassLoader将会按照这种方式最终委派给顶层的启动类加载器,只有当超类加载器无法执行加载时,才会将类加载任务退回给MyClassLoader执行。在重写findClass()方法时指定了以流的形式读取目标类型的二进制字节流到JVM内部,然后通过defineClass()方法将其转换为一个类的Class对象实例。
定位NoClassDefFoundError异常
一般情况下,这类异常往往是因为开发人员在程序中显式地使用了Class.forName()方法或者ClassLoader.getSystemClassLoader().loadClass()方法显式地加载一个类型所引起的,那么如果程序中一旦出现了该异常时,也就意味着类加载器执行类加载任务失败,因为类加载器无法根据所指定的全限定名正确地加载到目标类型。
触发ClassNotFoundException异常
/**
* 触发ClassNotFoundException异常
*/
public class ClassNotFoundTest {
public static void main(String[] agrs) {
try {
Class.forName("java.lang.Object_");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
程序输出如下:
Exception in thread "main" java.lang.ClassNotFoundException: java.lang. Object_
at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at java.lang.ClassLoader.loadClass(ClassLoader.java:423)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:356)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:188)
at ClassNotFoundTest.main(ClassNotFoundTest.java:8)
上述代码示例中,在forName()方法中指定了一个全限定名为java.lang.Object_的类型,只不过该类型并不存在,那么当AppClassLoader在ClassPath目录中找不到该类型时(开发人员可以在程序中使用System.getProperty(“java.class.path”)方法查阅当前的ClassPath目录),就意味着加载任务执行失败,最终程序就会抛出java.lang.ClassNotFoundException异常。
定位NoClassDefFoundError异常
在下面代码中,引用了Spring的一些相关构件用以初始化IoC容器,尽管能够成功通过编译,但是程序在运行的过程中,如果无法正确引用到所依赖的相关Spring构件时,将会抛出java.lang.ClassNotFoundException异常。
触发NoClassDefFoundError异常
/**
* 触发 NoClassDefFoundError异常
*/
public class NoClassDefFoundTest {
public static void main(String[] args) {
/* 加载Spring的Ioc配置文件,并初始化Ioc容器 */
new org.springframework.context.support.
ClassPathXmlApplicationContext("root-context.xml");
}
}
程序输出如下:
Exception in thread "main" java.lang.NoClassDefFoundError:
org/springframework/context/support/ClassPathXmlApplicationContextat
NoClassDefFoundTest.main(NoClassDefFoundTest.java:10)Caused by:java.lang. ClassNotFoundException:
org.springframework.context.support.ClassPathXmlApplicationContext
at java.net.URLClassLoader$1.run(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 1 more
当上述代码成功通过编译后,在使用命令“java NoClassDefFoundTest”执行之前,只要“切断”程序与第三方构件之间的依赖时,就会抛出java.lang.NoClassDefFoundError异常。所以大家在实际的开发过程中,尤其是在生产环境中执行项目部署时,一定要非常细心,确保项目中所依赖的所有第三方构件都能够被成功引用到。

