在Java中,类加载器是用来通过一个类的全限定名来获取描述此类的二进制字节流的代码模块。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。换句话说,比较两个类是否相等,只有在这两个类是被同一个类加载器加载时才有意义,否则即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类必定不相等。
下面我们用一个例子来了解一下被不同类加载器加载的同一个类会有什么影响:
package com.wxueyuan.classloader;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception{
ClassLoaderTest instance = new ClassLoaderTest();
ClassLoader myLoader = new ClassLoader() {
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null) return super.loadClass(name);
byte[] b = null;
try {
b = new byte[is.available()];
is.read(b);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return defineClass(name, b, 0,b.length);
}
};
Object obj = myLoader.loadClass("com.wxueyuan.classloader.ClassLoaderTest").newInstance();
System.out.println(instance.getClass());
System.out.println(obj.getClass());
System.out.println(instance.getClass().equals(obj.getClass()));
System.out.println(instance instanceof com.wxueyuan.classloader.ClassLoaderTest);
System.out.println(obj instanceof com.wxueyuan.classloader.ClassLoaderTest);
System.out.println(instance.getClass().getClassLoader());
System.out.println(obj.getClass().getClassLoader());
}
}
这段程序的运行结果如下:
class com.wxueyuan.classloader.ClassLoaderTest
class com.wxueyuan.classloader.ClassLoaderTest
false
true
false
sun.misc.Launcher AppClassLoader@73d16e93com.wxueyuan.classloader.ClassLoaderTest 1@7852e922
从前两句输出我们可以看出,instance对象和obj对象都是class com.wxueyuan.classloader.ClassLoaderTest这个类的实例。
但是当我们尝试instance.getClass().equals(obj.getClass())却得到了false,这两个实例的类并不相等,这就是因为ClassLoaderTest这个类是由不同的类加载器加载的,换句话说在Java虚拟机中存在着两个不同的ClassLoaderTest类。
当我们分别用instance和obj对象进行instanceof操作时我们发现instance返回true,而obj返回false;这是因为默认的 com.wxueyuan.classloader.ClassLoaderTest这个类是由系统应用程序类加载器加载的,而obj这个实例其实是我们用myLoader类加载器加载的类的实例。最后两行打印输出了这两个对象所属的不同的类加载器。
从Java虚拟机的角度来看,类加载器可以分为两类:
从Java开发人员的角度来看,类加载器可以分为四类:
上面介绍的4中类加载器之间存在着一种层次关系,这种层次关系也被称为双亲委派模型(Parents Delegastion Model),如下图所示:
双亲委派模型要求除了顶层的启动类加载器之外,其它所有的类加载器都必须有自己的父加载器。当一个类加载器收到了类加载的请求之后,它首先不会尝试自己加载这个类,而是将这个请求委派给父类加载器去完成,由于每一个类加载器都有这个逻辑,因此所有的类加载请求最终都应该传送到顶层的启动类加载器中。只有在父加载器在它的搜索范围内没有找到所需的类时,子加载器才会尝试去加载。
双亲委派模型对Java程序的稳定运行很重要,因为即使我们创建了与类库中全限定名相同的类,我们的类也不会被加载而影响到整个Java的运行环境。它的实现逻辑都集中在java.lang.ClassLoader.loadClass()方法中
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) {
//调用父类加载器的loadClass
c = parent.loadClass(name, false);
} else {
//如果父类加载器为空,则使用启动类加载器作为父加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//在父类加载器无法加载的时候,再调用findClass来加载类
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;
}
}
尽管双亲委派模型对Java程序的运行十分重要,但是在一些情况下,我们不得不对双亲委派模型进行破坏。比如双亲委派模型能够保证越基础的类越由上层的加载器加载,基础的类库之所以基础,是因为它们提供了大量的用户需要调用的API,但是在一些情况下基础的类库有可能要反过来调用用户提供的代码又该怎么办呢?
有的同学可能会说哪有基础类库需要调用用户提供的代码的时候,但事实上这种情况确实存在。比如SUN公司提供JNDI服务用来对命名服务或目录服务的资源进行管理和查找,它的代码是由启动类加载器去加载的(rt.jar),但是它需要调用由服务供应商提供的SPI的实现类(JNDI相关知识请关注此处)。
为了解决这个问题,JAVA设计团队提供了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时未设置,则会继承父线程的ContextClassLoader。
有了线程上下文类加载器,JAVA就可以实现父类加载器请求子类加载器去完成某些类的加载动作,这种行为相当于逆向使用了双亲委派模型的加载顺序。
在这篇博客中博主与大家一起学习了一下Java中的类加载机制,以及完成类加载动作的类加载器和传统的双亲委派模型,事实上,类加载的过程包括着“加载”,“验证”,“准备”,“解析”和“初始化”等5个阶段,博主在这里就不赘述了,感兴趣的同学可以自己下去查些资料去更深一步地了解类加载的过程。