关于这个问题,网上有很多种说法,甚至我看过某国内非常知名的付费专栏中,关于这个点也并不是讲解的特别清楚。那么,这里我们先总结一下Tomcat的类加载机制,然后再来证明为啥我这么说:
Tomcat的类加载机制,在默认情况下,是先把当前要加载的类委托给BootstrapClassLoader尝试加载,为了避免JRE中的核心类被我们应用自己给覆盖(如String等),Bootstrap如果无法加载,那么就由WebAppClassLoader尝试加载,如果无法加载,那么再委托通过双亲委派的方式向上委派给Common、System等类加载进行加载,即顺序为:Bootstrap->WebApp->System->Common**
上面的是默认情况,tomcat中有一个配置**<font style="color:#F38F39;">delegate</font>**
,他的默认值是false,如果设置成true了,那么他就会严格遵守双亲委派,按照Bootstrap->System->Common->WebApp的顺序进行加载。**
talk is cheap,show me the code
以下是tomcat中WebappClassLoaderBase.java中loadClass的代码,我做了一些精简,并加了一些注释:
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//加锁,防止并发
synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
if (log.isDebugEnabled()) {
log.debug("loadClass(" + name + ", " + resolve + ")");
}
Class<?> clazz = null;
// ...
// 检查本地缓存是否已加载该类,如果是,则直接返回缓存中的 Class 对象。
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Returning class from cache");
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
// 检查另一个类加载缓存,如果是GraalVM环境,直接返回缓存中的 Class 对象。
clazz = JreCompat.isGraalAvailable() ? null : findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Returning class from cache");
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
/*
* 尝试使用Bootstrap类加载器加载类,以防止Web应用程序覆盖Java SE类。如果加载成功,则返回加载的 Class 对象。
*/
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
URL url = javaseLoader.getResource(resourceName);
tryLoadingFromJavaseLoader = url != null;
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
boolean delegateLoad = delegate || filter(name, true);
// 根据 delegate 属性和其他条件判断是否应该委派加载给父类加载器。
// 如果需要委派,则直接先进行委派
if (delegateLoad) {
if (log.isDebugEnabled()) {
log.debug(" Delegating to parent classloader1 " + parent);
}
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Loading class from parent");
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// 自己尝试加载
// 能走到这里,肯定是BootStrap没加载到,之后还有两种情况:
// 1、如果delegate为ture的话,说明上层类加载器也没记载到。
// 2、如果delegate为false,那么就还没有进行过委派,先在这里尝试自己加载。
if (log.isDebugEnabled()) {
log.debug(" Searching local repositories");
}
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Loading class from local repository");
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 如果delegate为false,说明还没有做过委派,那么委派给父类加载器加载类。
if (!delegateLoad) {
if (log.isDebugEnabled()) {
log.debug(" Delegating to parent classloader at end: " + parent);
}
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled()) {
log.debug(" Loading class from parent");
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
整个代码的过程就是:
(图中红线和绿线是2选一分别执行的,不会同时执行,也不会交叉执行。)
以上,就是Tomcat的类加载机制。你说他打破双亲委派了么?
打破了,当delegate = false的时候,打破了双亲委派。但是也并不是上来就自己直接加载,而是也得先给老大哥——BootStrap尝试加载,避免JRE中的类被覆盖。
没打破,当delegate = true的时候,他是严格的遵守了双亲委派的。
一个Tomcat,是可以同时运行多个应用的,而不同的应用可能会同时依赖一些相同的类库,但是他们使用的版本可能是不一样的,但是这些类库中的Class的全路径名因为是一样的,如果都采用双亲委派的机制的话,是无法重复加载同一个类的,那么就会导致版本冲突。
而为了有更好的隔离性,所以在Tomcat中,每个应用都由一个独立的WebappClassLoader进行加载,这样就可以完全隔离开。而多个WebAppClassLoader之间是没有委派关系的,他们就是各自加载各自需要加载的Jar包。
由于每个Web应用程序都有自己的类加载器,因此不同Web应用程序中的类可以使用相同的类名,而不会产生命名冲突。
同时,由于每个Web应用程序都有自己的类加载器,因此在卸载一个Web应用程序时,它的所有类都会从内存中清除,这可以避免内存泄漏的问题。
这种层次化的类加载器结构和委派机制确保了类的唯一性和隔离性,避免了类的重复加载和冲突,同时也实现了多个Web应用程序的隔离和独立运行。
因为每个应用都是用WebAppClassLoader独自加载的,但是如果有一个公共的jar包,比如Spring,各个应用的版本都一样,那么岂不是要重复加载很多次了?这不是浪费么?
Tomcat给了个方案,那就是SharedClassLoader,我们可以把可以指定一个目录,让SharedClassLoader来加载,他加载的类在各个APP中都是可以共享使用的。