双亲委派与线程上下文类加载器

类加载器

Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载。

java中的类大致分为三种: 1.系统类 2.扩展类 3.由程序员自定义的类

java类加载器又分:

1)Bootstrap ClassLoader
负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

2)Extension ClassLoader
负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

3)App ClassLoader
负责记载classpath中指定的jar包及目录中class

4)Custom ClassLoader
属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

由此可见,会首先把保证程序运行的基础类一次性加载到jvm中。而根据资料java最早就是为嵌入式系统而设计的,内存宝贵。所有如果开始就把所有,用的到、用不到的类都加载到jvm中,势必会占用很多宝贵的内存,而且有些class可能压根在整个运行过程中都不会使用。


父类加载器加载的类不能访问子类加载器加载的类

在学习线程上下文类加载器的时候,经常看到网上的博文说父类加载器加载的类不能访问子类加载器加载的类,因此需要线程上下文类加载器。想请问该怎么理解这句话呢?

自我感觉,这么说有点绝对,只能说父类加载器想要使用 还未加载的 非JAVA_HOME/lib下的 类 是无法使用的。如果在项目加载过程中代码比如使用class.forname(.)加载了该类,那么父类是可以直接使用的。

当使用Bootstrap加载器加载一个对象并使用时,该对象内部要使用在classpath下(需要Application加载器加载)还未加载的一个类对象,但是根据双亲加载机制boostrap尝试加载该类因为其自身为最高层加载器所以只能有boostrap加载器加载,所以是无法加载不到该类的,也就无法使用该类,可以说父类是没有办法使用子类加载器加载的对象的。

这时候就需要contextClassloader了。

stackOverflow上:
The thread context classloader is the current classloader for the current thread. An object can be created from a class in ClassLoaderC and then passed to a thread owned by ClassLoaderD. In this case the object needs to use Thread.currentThread().getContextClassLoader() directly if it wants to load resources that are not available on its own classloader.


破坏双亲委派模型的情况

在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接这样就可以了:

 Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

可以看到这里直接获取连接,省去了上面的Class.forName()注册过程。
现在,我们分析下看使用了这种spi服务的模式原本的过程是怎样的:

第一,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
第二,加载这个类,这里肯定只能用class.forName(“com.mysql.jdbc.Driver”)来加载
好了,问题来了,Class.forName()加载用的是调用者的Classloader,这个调用者DriverManager是在rt.jar中的,ClassLoader是启动类加载器,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。

那么,这个问题如何解决呢?按照目前情况来分析,这个mysql的drvier只有应用类加载器能加载,那么我们只要在启动类加载器中有方法获取应用程序类加载器,然后通过它去加载就可以了。这就是所谓的线程上下文加载器。
线程上下文类加载器可以通过Thread.setContextClassLoaser()方法设置,如果不特殊设置会从父类继承,一般默认使用的是应用程序类加载器

很明显,线程上下文类加载器让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派模型的原则


SPI整体机制图如下:
在这里插入图片描述

SPI扩展机制应用场景有很多,比如Common-Logging,JDBC,Dubbo,Cipher等等。


tomcat

在⼀个tomcat⾥,有两个war包,都⽤的同⼀个版本的mysql驱动,tomcat重启的时候,两个war包会有⼀个先调⽤DriverManager.getConnection(),接着另⼀个调⽤。

war1调⽤DriverManager.getConnection()

DriverManager通过SPI机制把所有的jdbc driver都加载⼀次,这时候使⽤的类加载器是war1的,我们记作war1loader,DriverManager通过war1loader加载mysql Driver,mysql Driver主动register⾃⼰,这时候registeredDrivers的结果如下:

registeredDrivers={"war1loader: com.mysql.cj.jdbc.Driver"}

接下来,war2调⽤DriverManager.getConnection(),因为Drivermanager已经初始化过了,所以SPI那⼀套流程不会走了。会遍历registeredDrivers,并且判断是否是⾃⼰加载的,war2的加载器为war2loader,在iisDriverAllowed中,会调⽤

class.forName("com.mysql.cj.jdbc.Driver", true, war2loader)

显然这个结果与已经存在的不⼀致,但是,我们⽤war2loader加载驱动,会再次调⽤Driver的初始化,

它继续调⽤register,所以现在的结果就是

registeredDrivers={"war1loader: com.mysql.cj.jdbc.Driver", "war2loader: com.mysql.cj.jdbc.Driver"}

因为registeredDrivers是CopyOnWriteList,循环会继续往下走,下⼀次就能走过isAllowed,然后可以调⽤connect。

Thread Context ClassLoader

前⾯多次出现了ContextClassloader,没有展开解释,ContextClassLoader这个机制不太好理解,我们 先来看⼀下双亲委派机制。
在这里插入图片描述

底层的类加载器要加载⼀个类时,先向上委托,有没有发现⼀个特点, 这种双亲委派机制,直接⽗加载器是唯⼀的,所以向上委托,是不会有⼆义性的(OSGI不在讨论范围内)。 但是,假如在上层的类(例如DriverManager,它是由bootstrap classloader加载的)⾥要加载底层的类,它会⽤⾃⼰的加载器去加载,对于SPI来说,它的实现类都是在下层的,需要由下层的classloader加载。

还是以DriverManager为例,假设它在⾃⼰的代码⾥调⽤(虽然没有在代码⾥写上mysql,但是只要把mysql的jar包放在这,Drivermanager最终会扫描到并且调⽤class.forName(“com.mysql.c.jdbc.driver”)的,只是它传了classloader):

class.forName("com.mysql.cj.jdbc.driver");

我们看forName的代码

@CallerSensitive 
public static Class<?> forName(String className) throws ClassNotFoundException { 
    Class<?> caller = Reflection.getCallerClass(); 
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller); 
}

此处会寻找caller的类,然后找它的classloader,DriverManager调⽤的forName,所以此处的caller就是DriverManager.class,但是我们知道DriverManager是bootstrap加载的,那此处获取classloader就是null。forName0是native⽅法,它发现classloader是null就尝试⽤bootstrap加载,但是我们要加载的是mysql的类,bootstrap肯定是不能加载的。

假设我们的委派链是个单纯的单链表,那么我们⽤⼀个双向链表向下委托就⾏了,但是这种机制的委托链并不是单链表,所以向下委托是有⼆义性的。

那怎么办呢?谁调⽤我,我就⽤谁的加载器,这个加载器放在哪呢,就跟线程绑定,也就是Thread Context ClassLoader。

所以DriverManager在实际调⽤forName的时候,要⽤ContextClassLoader。 它⼀共有两处会加载类

⼀处是类初始化调⽤ServiceLoader的时候,我们知道ServiceLoader使⽤的是contextClassloader。

public static <S> ServiceLoader<S> load(Class<S> service) { 
    ClassLoader cl = Thread.currentThread().getContextClassLoader(); 
    return ServiceLoader.load(service, cl); 
}

⼀处是getConnection的时候,先检查⼀下caller的classloader,如果是null的话就使⽤ContextClassloader,在isDriverAllowed⾥加载类

// Worker method called by the public getConnection() methods. 

private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws 
SQLException { 
	/*
	* When callerCl is null, we should check the application's 
	* (which is invoking this class indirectly) 
	* classloader, so that the JDBC driver class outside rt.jar 
	* can be loaded from here. 
	*/ 
	ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; 
	synchronized(DriverManager.class) { 
	// synchronize loading of the correct classloader. 
		if (callerCL == null) { 
			callerCL = Thread.currentThread().getContextClassLoader(); 
		} 
	}
	..... 
	if(isDriverAllowed(aDriver.driver, callerCL)) { 
	.....

Thread Context ClassLoader意义就是:⽗Classloader可以使⽤当前线程Thread.currentthread().getContextLoader()中指定的classloader中加载的类。颠覆了⽗ClassLoader不能使⽤⼦Classloader或者是其它没有直接⽗⼦关系的Classloader中加载的类这种情况。这个就是Thread Context ClassLoader的意义。⼀个线程的默认ContextClassLoader是继承⽗线程的,可以调⽤set重新 设置,如果在main线程⾥查看,它就是AppClassLoader。

在这里插入图片描述

在这里插入图片描述

参考文章:
1、https://segmentfault.com/q/1010000019872768
2、https://segmentfault.com/q/1010000019878600/
3、https://segmentfault.com/q/1010000013805821?utm_source=sf-similar-question
4、https://www.cnblogs.com/muzhongjiang/p/15060232.html
5、https://blog.51cto.com/nxlhero/2697891
6、https://blog.csdn.net/weixin_39312465/article/details/84838181

猜你喜欢

转载自blog.csdn.net/yangyangrenren/article/details/121325624
今日推荐