最近在阅读微信公众号的时候,看见一个问题,TOMCAT中为什么要破坏双亲委派机制?看后发现自己根本不知道,然后在网上总结资料整理出这篇文章。
前言
双亲委派:指的是子加载器向父加载器和父加载器无法加载时子加载器加载的一个过程。
类加载器类型划分
从虚拟机层面讲,可以分为两大类加载器,一、是BootStrap ClassLoader,即启动类加载器(c++实现),他是虚拟机的一部分。二十其他类加载器(java实现),在虚拟机外部,并且全部继承ClassLoader类。
细细划分的话可以分为三类:
1)bootStrapClassLoader
启动类加载器,完全由jvm控制加载,外面访问不到这个类加载器,即不能被java程序引用。它主要负责加载jvm自身的工作类,即java/lib目录和-Xbootclasspath参数指定的目录的类库。
2)ExtensionClassLoader
扩展类加载器,由java实现,即ExtClassLoader实现类。它主要负责加载java/lib/ext目录和系统环境变量java.ext.dirs指定目录所有类库。
3)ApplicationClassLoader
应用程序类加载器,由java实现,即AppClassLoader实现类。它的父类是ExtClassLoader,它主要负责加载classpath目录上的类库。如果没有自定义ClassLoader,它就是程序中默认的ClassLoader,即可以通过ClassLoader.getSystemClassLoader()获取当前系统的类加载器。
类加载机制
- 什么是双亲委派机制?
首先我们需要知道:任何一个类,都需要由加载它的类加载器和这个类本身在jvm(java虚拟机)中确定唯一性。
当一个类加载器收到一个类加载请求,该类加载器并不会对这个类进行加载,而是传递该类加载器的父类加载器,每一个层次的类加载器都是如此,因此每一个类的加载请求都会传递到类加载器的顶端,只有当父类加载器在其搜索范围内没有找到类信息,才会反馈给子类加载器,由子类加载器去加载。
- 双亲委派机制模型的好处
java类随着加载它的类加载器有了一个优先层次关系。例如:java.lang.Object.她是在rt.jar包中,无论哪一个类要加载它,最终都会由顶级的BootStrapClassLoader去加载,也就导致Object类在程序中的其他类加载器环境中只有一个。如果不是双亲委派机制的话,假如用户自己也创建了一个Object,并且由自己定义的类加载器加载到环境中,就会造成多个Object类型,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
JDBC为什么破坏双亲委派机制?
问题背景
在JDBC 4.0之后实际上我们不需要再调用Class.forName来加载驱动程序了,我们只需要把驱动的jar包放到工程的类加载路径里,那么驱动就会被自动加载。
这个自动加载采用的技术叫做SPI,数据库驱动厂商也都做了更新。可以看一下jar包里面的META-INF/services目录,里面有一个java.sql.Driver的文件,文件里面包含了驱动的全路径名。
使用上,我们只需要通过下面一句就可以创建数据库的连接:
Connection con = DriverManager.getConnection(url , username , password ) ;
问题解答
因为类加载器受到加载范围的限制,在某些情况下父类加载器无法加载到需要的文件,需要委托子类加载器去加载class文件。
JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包。DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于 $JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的 Jar 包,根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。也就是说BootStrap类加载器还要去加载jar包中的Driver接口的实现类。我们知道,BootStrap类加载器默认只负责加载 $JAVA_HOME中jre/lib/rt.jar 里所有的class,所以需要由子类加载器去加载Driver实现,这就破坏了双亲委派模型。
查看DriverManager类的源码,看到在使用DriverManager的时候会触发其静态代码块,调用 loadInitialDrivers() 方法,并调用ServiceLoader.load(Driver.class) 加载所有在META-INF/services/java.sql.Driver 文件里边的类到JVM内存,完成驱动的自动加载。
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
这个子类加载器是通过 Thread.currentThread().getContextClassLoader() 得到的线程上下文加载器。
那么这个上下文类加载器又是什么呢?
public Launcher() {
...
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
...
}
Tomcat为什么要破坏双亲委派模型
每个Tomcat的webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。
从图片中能看到TOMCAT自己重新定义了好多的ClassLoader,为什么会重新定义这么一些呢,大致处于一下三个目的:
- 对于不同webapp中的lib目录下的jar包,需要相互隔离,不能出现一个应用中的jar包加载影响了另外一个应用。对于相同的资源,则加载一份,一遍造成资源浪费。eg:如果webapp1和webapp2都用到了log4j,可以将log4j提到tomcat/lib中,表示所有应用共享此类库,试想如果log4j很大,并且20个应用都分别加载,那实在是没有必要的。
- 与jvm一样,使用特定的类加载器去加载tomcat本身相关的jar包,避免恶意或者无意的破坏。
- 和热部署相关。