回顾
上一篇博客复习了JVM的类加载过程,对加载、验证、准备、解析、初始化等各个阶段进行了详细内容的复习。但是,关于类加载,还有一块重要的知识——类加载器,它跟加载阶段、解析阶段等有着紧密的联系。
本篇博客将会对类加载器的分类、双亲委派模型以及打破双亲委派等方面进行复习。
类加载器的分类
我们常把类加载器分为四类:
- 启动类加载器(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 系统类加载器(Application ClassLoader)
- 自定义类加载器
加载范围
启动类加载器:加载<JAVA_HOME>/lib/*.jar,也就是jre文件夹下lib中的jar包,如rt.jar。此外,它还可以加载参数-XBootstrapPath指定的路径的jar包。
扩展类加载器:加载<JAVA_HOME>/lib/ext/*.jar。
系统类加载器:加载classpath下的jar包,也就是我们平常写java程序时,build path的jar包。一般情况下,我们自定义的类和第三方包都是由该加载器加载的。
双亲委派模型
类加载器的关系
启动类加载器是JVM内置的,使用C++程序编写,并不是扩展类加载器的父类。扩展类加载器是应用类加载器的父类。
/*
测试类加载器的关系
*/
package classloadtest;
public class ClassLoaderRelationTest {
public static void main(String[] args) {
//获取应用类加载器
ClassLoader c1 = ClassLoaderRelationTest.class.getClassLoader();
//查看应用类加载器的父类
ClassLoader c2 = c1.getParent();
//查看应用类加载器的父类是否有父类
ClassLoader c3 = c2.getParent();
System.out.println(c1);
System.out.println(c2);
System.out.println(c3);
}
}
输出结果如下:
可以看到自定义的类ClassLoaderRelationTest是由Application ClassLoader加载的,它的父类加载器为Extension ClassLoader,而再往上就为null了,说明Bootstrap ClassLoader并不是它的父类加载器。
双亲委派模型
在类加载器加载类的过程中,首先会将加载任务委托给该加载器的父类加载器,每一层都这样,直到交给Extension ClassLoader,这时它的父类加载器为null,然后交给Bootstrap ClassLoader。如果上层的类加载器无法加载这个类,就逐级往下又将加载任务交给子类加载器来完成(在代码中即调用classLoader的findClass方法)。
过程很简单,上ClassLoader的源码,一看就明白了!
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name); //查看是否已经加载了这个类
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false); //将加载任务交给父类的逻辑
} else {
c = findBootstrapClassOrNull(name); //父类为null就给启动类加载器
}
} 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();
c = findClass(name); //调用自己的findClass方法,自己来加载
// 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;
}
}
打破双亲委派模型
第一次打破
第一次打破双亲委派模型在JDK1.2之前,那个时候,人们继承ClassLoader的唯一目的就是重写loadClass方法,也就是我们上面贴的源码(当然相比1.2有了很多改动,特别是JDK1.2之前并没有findClass方法)。这里有两点需要注意:
人们为什么要重写loadClass方法呢?
因为JVM内部在加载类时,会调用类加载器的私有方法loadClassInternal,这个方法的唯一逻辑就是去调用自己的loadClass方法。
为什么不说这样就打破了双亲委托机制呢?
在我们上面的源码中可以看到,双亲委托机制就是靠loadClass方法的内部逻辑来实现的,如果你重写loadClass方法,还有这个逻辑吗?
官方解决办法
在JDK1.2以后,官方引进了findClass方法,推荐用户不要重写loadClass方法,而去重写findClass方法。(但是如果你执意要重写loadClass方法,仍然可以重写)。个人认为,这有点像借鉴了设计模式中的模板方法模式,即,我将算法框架固定住了,你只用实现里面的某个具体逻辑就行了。
第二次打破
第二次打破双亲委派机制的是SPI(Service Provider Interface),服务提供接口,JavaSPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。我们常见的JDBC就是典型。
SPI为什么要打破双亲委托机制呢?
以JDBC为例,MySQL的驱动包实现了SPI的Driver接口,DriverManager需要加载Driver接口的实现类,然而DriverManager是有启动类加载器加载的,上篇博客中写到的类和接口的解析过程,在C类中有一个D类的符号引用需要解析,C类会首先判断D类是否被加载,若没有,则先使用C类的加载器去加载D类。所以这里会使用启动类加载器去加载MySQL的驱动包,这个包根本不在bootstrap classloader的加载路径下!!所以无法加载成功。这就是《深入理解java虚拟机》中说的基础类中回调了用户代码。
官方如何解决?
官方引入了一个线程上下文加载器(contextClassLoader),这个可由java.lang.Thread类使用getClassLoader获取和setClassLoader设置,如果没有设置,那么就是从父线程中继承一个。如果程序的全局范围内都没有设置,则默认为application classLoader。这样,在DriverManager中就可以调用getContextClassLoader获取线程上下文类加载器来加载Driver的实现类了!
第三次打破
在所谓的“热部署”、“热替换”中,人们想实现插件式的代码,比如OSGI中每一个模块(bundle)都有自己的一个类加载器。
总结
到此就将类加载的相关基础知识复习完了!