一 概述
双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者的类加载方式。Java中的大部分类的加载器都遵循这个模型。例外的是,直到Java模块化出现为止,双亲委派模型主要出现过3次比较大规模的"被破坏"的情况。
二 双亲委派机制的第一次破坏
双亲委派模型的第一次"被破坏"其实发生在双亲委派模型出现之前,即JDK1.2之前。由于双亲委派模型在JDK1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一版本中已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术方式避免loadClass()方法被子类覆盖的可能性,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引到用户编写的类加载逻辑尽可能去重写这个方法,而不是在loadClass()中编写代码。双亲委派的具体逻辑就实现在loadClass()方法中,按照loadClass()方法的逻辑,如果父加载器失败,会自动调用自己的findClass()方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写的类加载器是符合双亲委派规则的。
自定义类加载重写的方法findClass()和defineClass()
//自定义类加载器重写的方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
//JDK自动实现方法,自定类加载器调用即可
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
双亲委派机制逻辑实现方法loadClass()
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//这里会涉及到多个线程使用同一个ClassLoader去加载同一个类,所以为该类添加同步锁
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
/*先从CustomClassLoader开始检查某类是否被加载,如没有然后到parent ClassLoader(AppClassLoader)
*中检查,如果也没有再到parent ClassLoader(ExtClassLoader)中检查。如果这三个ClassLoader中都没有
* 加载该类,就会到BootStrapClassLoader(C++编写并不是其他加载器的父类)中去检查。
* */
/*
* CustomClassLoader
* */
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
/*
* AppClassLoader->ExtClassLoader
* */
c = parent.loadClass(name, false);
} else {
//BootStrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
/*
*BootStrapClassLoader->ExtClassLoader,AppClassLoader,CustomClassLoader
* 按需求使用各自实现代findClass(name)方法
* 来加载在加载器中没有被加载到类
* */
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
三 双亲委派机制的第二次破坏
这次破坏是由这个模型自身的缺陷导致,因为双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器加载),基础类型被称为基础是因为它们总是被用户代码继承,作为调用的API来使用,但是程序设计往往没有绝对不变的规则,存在基础类型又要回调用户的代码的情况。
这种情况典型的例子便是JNDI服务(Java Naming and Directory Interface,Java命名和目录接口),它是由启动类加载器即(BootStrap ClassLoader)来完成加载(JDK1.3时加入到rt.jar中),作为Java中的基础类型,其目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在的问题是启动类加载器不认识这些被调用的ServiceProviderInterface。
为了解决这个问题,Java的设计团队只好引入了一个不太优雅的设计:线程上下文加载器(Thread Context ClassLoader)这个类加载可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时未被设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有被设置的时候,那这个类加载器默情况认就是应用程序类加载器。
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
有了线程上下文加载器,程序就可以做一些"舞弊"的事情。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器完成类去请求子类加载器完成类加载的行为,这种行为实际上打通了双亲委派机模型层次结构来逆向使用类加载器,这是一种违背双亲委派模型的一般性原则。Java中涉及SPI的加载基本上都采用了这中方式来完成,例如JNDI,JDBC,JCE,JAXB和JBI等。不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来实现硬编码判断,为了消除这种极不优雅的实现方式,在JDK1.6的时候,JDK提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式来完成这部分工作。
/**
* @author Mark Reinhold
* @since 1.6
*/
public final class ServiceLoader<S> implements Iterable<S>{
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
}
四 双亲委派模型的第三次破坏
双亲委派模型的第三次破坏是由于用户对程序动态性的追求而导致的,动态性是指一些比较热门的名词:代码热替换(Hot Swap),模块热部署(Hot Deployment)等。也就是希望Java应用程序能像我们的电脑外设那样,接上鼠标,U盘,不同重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用关机也不用重启。这样可以避免线上的系统因为重启而发生的事故。
OSGi(Open Service Gateway Initiative 开放服务网关)实现模块化热部署的关键是它自定的类加载机制的实现,每一个程序模块(OSGi中称为Bundle(英文为(束)))都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。
OSGi类加载时类搜索的顺序:
- 将java.*开头的类,委派给父类加载器加载。
- 否则,将委派列表名单内的类,委派给父类加载器加载。
- 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
- 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Frangment Bundle的类加载器加载。
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
- 否则,类查找失败。
上面的查找顺序中只有1和2符合双亲委派机制的原则,其余的类查找都是在平级的类加载器中进行的。
五 个人总结
第一次破坏双亲委派机制是针对loadClass()方法,为了避免直接在loadClass()方法中增加逻辑代码,所以引入findClass()方法使得用户基于该方法实现自定义类加载器,从而保证符合双亲委派机制。
第二次破坏双亲委派机制是像JNDI服务一样,基础代码使用用户代码,利用线程上下文加载器实现父类加载器请求子类加载器进行类加载。
第三次破坏双亲委派机制是因为类似OSGi实现模块化热部署等功能,每一个程序模块都有自己的类加载器,当需要换模块的时候,就会将模块连同类加载器一起换掉以实现的热部署。使得类加载器不再是双亲委派模型推荐的树状结构,而是进一步发展称为更加复杂的网状结构。