双亲委派机制和SPI的理解
双亲委派机制基本原理
双亲委派机制简单的来说是JVM类加载过程的一个非强约束模型。子类加载器加载对应的Class,不会直接加载,首先会交由其父类加载器加载,若父类加载器无法加载,则由自己加载。
Java一共有三层类加载器,级别从上往下依次是:
-
BootStrap Class Loader:该类加载器由C++实现,在java语言中无法感知,所以在获取类加载器为
null
,主要用于加载<JAVA_HOME>\lib
目录下的类。当然也可自己通过JVM参数指定。
-
ExtClassLoader:扩展类加载器,主要用于加载
<JAVA_HOME>\lib\ext
目录下的类。 -
AppClassLoader:系统类加载器,用大白话来讲,就是加载我们自己定义的类的类加载器还有引入的一些第三方jar包。
具体的示例代码如下:
System.out.println(HashMap.class.getClassLoader()); // null System.out.println(JarFileSystemProvider.class.getClassLoader()); //sun.misc.Launcher$ExtClassLoader@355da254 System.out.println(Main.class.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
双亲委派机制的具体实现可以参考ClassLoader#loderClass方法,具体代码如下:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> 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 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);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}
不难发现,在Jdk源码中,类加载器自身加载类的方法其实findClass
,因此官方推荐自定义类加载器的时候,建议重载findClass
,而不是loadClass
方法,因为lodaClass
方法重载后,若开发者没有按照规范开发,便会打破双亲委派机制。
双亲委派机制存在意义
-
【有序性】首先,双亲委派机制能够有效的避免一个类被重复加载,一个类只会被加载一次。若一个类被父类加载器加载,对于子类加载器其实是无感的,若子类加载器直接加载对应的class,会导致一个类被重复加载。
-
【安全性】同样也是处于安全性考虑,如果JDK和核心类库,被其他类加载器加载,Java中最基础的行为也就无法保证。在双亲委派的场景下,能够确保被
BootStrap
类加载器加载,就不会出现上述问题。
双亲委派的破坏
-
通过自定义类加载器,重写
loadClass
来破坏双委派。 -
JNDI (Java Naming and Directory Interface,Java命名和目录接口) 需要服务者提供接口实现代码(SPI)。这里一个典型的应用就是JDBC的Driver驱动实现,各个数据库厂商可以针对JavaEE中的规范自行实现。这里主注意的是只有Java类库中的SPI才会对双亲委派进行破坏,自定义接口和第三方Jar包并不会对双亲委派机制进行破坏。以Driver的SPI为例,参考
ServiceLodar#nextService
:破坏双亲委派的代码就是
Class.forName(cn, false, loader)
这一行。传入的是一个loader,默认是系统类加载器。
private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {// 双亲委派被破坏c = Class.forName(cn, false, loader);} catch (ClassNotFoundException x) {fail(service,"Provider " + cn + " not found");}if (!service.isAssignableFrom(c)) {fail(service,"Provider " + cn + " not a subtype");}try {S p = service.cast(c.newInstance());providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error(); // This cannot happen}
如果直接写出
Class.forName(cn)
,那他的加载器就是ServiceLoader的类加载器,而ServiceLoader位于<JAVA_HOME>\lib
,它的类加载器是BootStrap类加载器,显然无法加载第三方jar包及其用户自定义代码,需要指定子类加载器,BooStrap类加载器将类加载委托给AppClassLodaer
。因此就造成双亲委派被破坏。 -
不难发现,由于双亲委派机制的存在,一个类至始至终只会被加载一次,因此当程序需要热更新时,双亲委派机制也就随之被破坏。