类加载器的作用就是把类的字节码加载到JVM。同时JVM规定,程序员可以用Java代码来自定义类加载器,把类的字节码信息加载到JVM中。
从java虚拟机角度来看,类加载器分成C++语言实现的启动类加载器属于虚拟机的而一部分,和java语言实现所有其他类的加载器,独立于虚拟机外部并且全部集成自抽象类java.lang.ClassLoader。
从开发者角度来看,类加载器分为以下3种:
负责加载系统的核心类(
负责加载系统
负责加载用户类。也就是我们平时java程序员自己写的代码程序类。
App ClassLoader的父类加载器是Ext ClassLoader,Ext ClassLoader的父类加载器是Boot ClassLoader。如下图:
在加载一个类的时候,如果加载器还有父类加载器,那么会尝试让父类加载器去加载该类。只有当父类没有加载过该类,并且父类也无法加载该类的时候,自己才会加载。
具体是什么意思,我们可以看如下代码:
public class ClassLoaderTest{
public static void main(String[] args) {
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
while (classLoader != null) {
System.out.println(classLoader);
// 获取父类加载器
classLoader = classLoader.getParent();
}
System.out.println(classLoader);
}
}
class ClassLoaderTest{
}
程序很简单,在main方法中打印ClassLoadTest的类加载器和父类加载器。这里有一个概念“一个类被加载时的默认类加载器,是和它外类类加载器是同一个”,所以此时执行程序后打印结果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@60e53b93
null
第一行结果说明:ClassLoaderTest使用的类加载器是AppClassLoader。
第二行结果说明:AppClassLoader父类加载器是ExtClassLoader
第三行结果说明:ExtClassLoader父类加器是Bootstrap ClassLoader,因为Bootstrap ClassLoader不是一个普通的Java类,所以ExtClassLoader的parent=null,所以第三行的打印结果为null就是这个原因。
此时,如果我们把ClassLoaderTest达成Jar包,然后丢到JAVA_HOME/jre/lib/ext目录下去,再执行这段程序,此时的结果会是什么呢?
此时打印的结果如下
sun.misc.Launcher$ExtClassLoader@60e53b93
null
当打印ClassLoaderTest的类加载器时,根据双亲委派首会传递到父类加载器Extension Class Loader;Extension ClassLoader会传递到它的父类Bootstrap Class Loader。因为Bootstrap ClassLoader是顶级的加载器,此时就去自己的类空间JAVA_HOME/jre/lib找,发现没有ClassLoaderTest后就告诉Ext我无能为力;Ext ClassLoader收到这个消息后就在自己类空间JAVA_HOME/jre/lib/ext找,发现有ClassLoaderTest这个类。所以就是用Ext ClassLoader加载。
类加载器虽然只用于实现类的加载作用,但是实际意义远远不限于类加载阶段。所有的类最终都会被加载到内存中,对于任意一个类如何确定它再内存中的唯一性是根据类的全限定名+同一类加载器。更通俗的来讲就是即使这两个类来源于同一Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。
java.lang.String是我们很常用的一个类,根据双亲委派的这种模型,所以无论哪一个类需要使用String,最终都会被Bootstrap ClassLoader所加载。相反如果没有使用双亲委派模型的话,由各个类加载器自行加载,那么系统中将会有多个不用的String类,java类型体系中最基础的行为也就无法保证,应用程序将会一片混乱。
现在ClassLoaderTest是ext目录下的一个扩展类,假如现在ClassLoaderTest类中有一个方法出现了一个A a = new A();那么根据双亲委派这个类A肯定是被Ext发起然后传递到Boot。但是如果这个类A再classpath下面,那么加载的时候就会爆出ClassNotFound。
你肯定会问,怎么还会有这样的情况?现实是存在这种情况的,JDBC中的类被Boot加载,最后还是需要到classpath下面去找驱动类。这就说明有一些系统级别的类需要反过来调用用户类。这时候就需要打破双亲委派模型了。
如果系统类需要加载用户类,那么此时要怎么做的?我们可以在线程上下文中,把App类加载器绑定,然后线程运行到需要A类这一行的时候,使用getContextClassloader把App类加载器取出来,然后使用App.loadClass()方法去加载。这就是线程上下文Classloader的作用,用来打破双亲委派模型。
public class ClassLoaderTest {
// 加载扩展Ext目录下的类
public void loadExtClass() throws Exception {
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println("方法loadExtClass使用的类加载器为" + classLoader);
// 加载ExtClass
Class> clazz = classLoader.loadClass("ExtClass");
Constructor> constructor = clazz.getConstructor();
System.out.println("ExtClass类正在加载" + constructor.newInstance());
}
}
class ExtClass {
public ExtClass() {
System.out.println("ExtClass执行构造方法...");
}
}
此时我们对之前放入ext目录下的ClassLoaderTest类进行了扩展,loadExtClass方法是加载了一个名字叫ExtClass的类,这个类再当前目录下面,我们查看它的类加载和方法。打包后丢入ext目录,然后写一个如下的类运行:
public class ClassLoaderDemo {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
Class> classLoaderTest = classLoader.loadClass("ClassLoaderTest");
// 利用反射得到类
Constructor> constructor = classLoaderTest.getConstructor();
ClassLoaderTest obj = (ClassLoaderTest)constructor.newInstance();
obj.loadExtClass();
}
}
这个类也非常简单,获取ClassLoaderTest的类加载器,并加载“ClassLoaderTest”,之后使用反射生成ClassLoaderTest的实例,最后执行loadExtClass方法得到如下结果:
方法loadExtClass使用的类加载器为sun.misc.Launcher$ExtClassLoader@1d44bcfa
ExtClass执行构造方法...
ExtClass类正在加载ExtClass@66d3c617
和我们上面理解的双亲委派模型一样,ClassLoaderTest所使用的类加载器为ExtClassLoader,并且使用ExtClassLoader成功加载了ExtClass。
接下来我们要来写一个复杂一点的案例了,如下:p
ublic class ClassLoaderTest {
// 加载classpath下的类
public void loadClassUnderClasspath(String name) throws Exception {
System.out.println("当前类的类加载器为" + ClassLoaderTest.class.getClassLoader());
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println("和当前线程上线文绑定的类加载器为" + contextClassLoader);
Class> aClass = contextClassLoader.loadClass(name);
Constructor> contextClassLoaderConstructor = aClass.getConstructor(ClassLoader.class);
// 执行构造器
contextClassLoaderConstructor.newInstance(contextClassLoader);
}
}
ClassLoaderTest中增加了一个loadClassUnderClasspath,意思就是用户传入当前目录下类的名字去加载。
系统类Ext需要去加载用户类,那么此时双亲委派就被打破,我们修改上文的程序,现在执行loadClassUnderClasspath
public class ClassLoaderDemo {
public static void main(String[] args) throws Exception {
// 多余代码同上
obj.loadClassUnderClasspath("MyDriver");
}
}
class MyDriver {
public MyDriver(ClassLoader classLoader) {
System.out.println("MyDriver正在被构造,使用的类加载器为" + classLoader);
}
}
打印结果如下
当前类的类加载器为sun.misc.Launcher$ExtClassLoader@1d44bcfa
和当前线程上线文绑定的类加载器为sun.misc.Launcher$AppClassLoader@18b4aac2
MyDriver正在被构造,使用的类加载器为sun.misc.Launcher$AppClassLoader@18b4aac2
执行loadClassUnderClasspath时候的类加载器为ExtClassLoader说明当前类的外围加载器为Ext ClassLoader。但是我们通过
Thread.currentThread().getContextClassLoader();
方法获取的和当前线程上下文绑定的类加载为App ClassLoader,此时就可以愉快的加载用户类空间classpath下的类了。
这个也是双亲委派模型自身缺陷导致的,为了解决这个困境,Java的设计团队只好引入了这个不太优雅的设计:线程上下文类加载器。如果应用程序在全局范围类没有设置过得话,那么这个类加载器默认就是App ClassLoader