说到Tomcat类加载机制,我们不得不提及JVM的类加载,然后Tomcat也是运行在JVM上的。所以,我们先抛砖引玉,一起来看看JVM类加载。
JVM类加载采用父类委托机制,当JVM运行过程中,用户需要加载某些类时,用户自己的类加载器,把加载请求传给父加载器,父加载器再传给其父加载器,一直到加载器树的顶层。最顶层的类加载器首先针对其特定的位置加载,如果加载不到就转交给子类。如果一直到底层的类加载都没有加载到,那么就会抛出异常ClassNotFoundException。
回过头来,我们再来看Tomcat类加载,Tomcat类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。
下面的简图是Tomcat9版本的官方文档给出的Tomcat的类加载器的图。
Bootstrap
|
System
|
Common
/ \
Webapp1 Webapp2 ..
Bootstrap :是Java的最高的加载器,用C语言实现,主要用来加载JVM启动时所需要的核心类,例如$JAVA_HOME/jre/lib/ext路径下的类。
System: 会加载CLASSPATH系统变量所定义路径的所有的类。
Common:会加载Tomcat路径下的lib文件下的所有类。
Webapp1、Webapp2……: 会加载webapp路径下项目中的所有的类。一个项目对应一个WebappClassLoader,这样就实现了应用之间类的隔离了。
这3个部分,在上面的Java双亲委派模型图中都有体现。不过可以看到ExtClassLoader没有画出来,可以理解为是跟bootstrap合并了,都是去JAVA_HOME/jre/lib下面加载类。那么Tomcat为什么要自定义类加载器呢?
隔离不同应用:部署在同一个Tomcat中的不同应用A和B,例如A用了Spring2.5。B用了Spring3.5,那么这两个应用如果使用的是同一个类加载器,那么Web应用就会因为jar包覆盖而无法启动。
灵活性:Web应用之间的类加载器相互独立,那么就可以根据修改不同的文件重建不同的类加载器替换原来的。从而不影响其他应用。
性能:如果在一个Tomcat部署多个应用,多个应用中都有相同的类库依赖。那么可以把这相同的类库让Common类加载器进行加载。
Tomcat自定义了WebAppClassLoader类加载器。打破了双亲委派的机制,即如果收到类加载的请求,会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载Web应用自己定义的类。我们知道ClassLoader默认的loadClass方法是以双亲委派的模型进行加载类的,那么Tomcat既然要打破这个规则,就要重写loadClass方法,我们可以看WebAppClassLoader类中重写的loadClass方法。
@Override
public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class clazz = null;
// 1. 从本地缓存中查找是否加载过此类
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// 2. 从AppClassLoader中查找是否加载过此类
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
String resourceName = binaryNameToPath(name, false);
// 3. 尝试用ExtClassLoader 类加载器加载类,防止Web应用覆盖JRE的核心类
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
URL url;
if (securityManager != null) {
PrivilegedActiondp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
tryLoadingFromJavaseLoader = true;
}
boolean delegateLoad = delegate || filter(name, true);
// 4. 判断是否设置了delegate属性,如果设置为true那么就按照双亲委派机制加载类
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
}
}
// 5. 默认是设置delegate是false的,那么就会先用WebAppClassLoader进行加载
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
}
// 6. 如果此时在WebAppClassLoader没找到类,那么就委托给AppClassLoader去加载
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);
}
我们总结起来就是Web应用默认的类加载顺序是(打破了双亲委派规则):
1.先从JVM的BootStrapClassLoader中加载。
2.加载Web应用下/WEB-INF/classes中的类。
3.加载Web应用下/WEB-INF/lib/*.jap中的jar包中的类。
4.加载上面定义的System路径下面的类。
5.加载上面定义的Common路径下面的类。
到此为止,Tomcat类加载机制逐渐明朗,Tomcat类加载机制的重点就是打破了双亲委派机制,WebAppClassLoader加载类的时候,绕开 AppClassLoader,直接先使用 ExtClassLoader 来加载类。这不仅保证了基础类不会被同时加载,也
保证了在同一个 Tomcat 下不同 web 之间的 class 是相互隔离的。好了。Tomcat的类加载机制就讲到这里,感兴趣的小伙伴可以去观看本站的Tomcat服务器教程,深入学习Tomcat里的各种技术。
你适合学Java吗?4大专业测评方法
代码逻辑 吸收能力 技术学习能力 综合素质
先测评确定适合在学习