记得前几天写getResourceAsStream()提到后面有时间整理下tomcat的ClassLoader的加载结构。今天有闲暇,笔者就来聊聊我认为的tomcat ClassLoader。
首先提到classloader,笔者首先想到的树的概念,每一层classloader都讲究着寻根方式,一般而言是一个类加载到jvm虚拟机的过程,那么对于一个类是否“相同”,取决于这两个类的类加载器是否相同,尽管一个类来源于同一个class文件,同一个jvm虚拟机加载,但是他们的classloader不一样,那么他们的类就是不相等的。以下面图1为例,可见当前的com.bes.enterprise.webtier.core.DefaultContext被CommonClassLoader加载,而该classloader的hash值为3967e60c。因此不同的classloader他们的hash值是不一样的,所以就会存在类在加载过程中的差异,那么为什么同一个tomcat部署多个应用,而应用中存在相同的jar及相同的类,也不会发生冲突,正是因为classloader的隔离,每个应用有独立的一个WebappClassLoader加载,他们相互隔离,因此不会出现类冲突的问题。基于具体的隔离原理,后续在做解释。
图1
Tomcat的ClassLoader层级(对与树形结构图可以查看博文下方图8的结构):
BootstrapClassLoader(启动类加载器):属于jdk层面的classloader,最顶层的classloader主要加载:${JAVA_HOME}/lib目录下的jar包,主要有rt.jar,resources.jar等。而rt.jar包含但不限于我们常用的java.util,java.lang,java.nio,java.io等包。BootstrapClassLoader的代码实现是native的,不是一个普通的java类,底层由c实现,因此jvm启动的时候,bootstrapClassLoader也会被同时带起。或者配置-XbootClasspath参数指定的路径也会被同时加载。查看下面图9我们通过jinfo pid | findstr sun.boot.class.path可以查看到bootstrapClassLoader主要加载了来自于${JAVA_HOME}/lib目录下的内容。(补充说明windows下用findstr linux下查看使用jinfo pid | grep name即可)
ExtensionClassLoader(扩展类加载器):隶属于sun.misc.Launcher的内部类,该classLoader继承了URLClassLoader,主要负责加载${JAVA_HOME}/lib/ext目录下的,或者加载系统属性java.ext.dirs中定义的目录。查看下面图9我们通过jinfo pid | findstr java.ext.dirs可以查看到ExtClassLoader主要加载了来自于${JAVA_HOME}/lib/ext的目录以及系统属性定义的目录或者jar。
ApplicationClassLoader(应用类加载器):隶属于sun.misc.Launcher的内部类,该classLoader继承了URLClassLoader,主要负责加载classpath环境变量定义的目录和jar,或者加载系统属性java.class.path中定义的目录。根据下面图9可以查看到Tomcat的AppClassLoader主要加载了来自于tomcat/bin下的jar包。
图2
CommonClassLoader(通用类加载器):隶属于org.apache.catalina.startup.Bootstrap类,tomcat的自有的classloader。主要负责加载${catalina.base}/lib定义的目录和jar以及${catalina.home}/lib定义的目录和jar,也可自定义配置,具体配置可见conf/catalina.properties中,里面的common.loader里指定了该classloader加载的jar和目录。
SharedClassLoader(共享类加载器):隶属于org.apache.catalina.startup.Bootstrap类,tomcat的自有的classloader。这个classloader功能主要用于多个web应用可能共享某些目录或者jar,这样你配置在shared.loader中,tomcat会将这些资源加载到sharedclassLoader中,用于实现应用层之间的共享。具体配置也是conf/catalina.properties中,里面的shared.loader里指定了该classloader加载的jar和目录。tomcat默认未配置该值。
CatalinaClassLoader:隶属于org.apache.catalina.startup.Bootstrap类,tomcat的自有的classloader。该classloader的加载和web应用隔离,加载的jar不会互相影响,可以查看下面图的层次结构,可以看到CatalinaclassLoader的parent为CommonClassLoader。用于实现tomcat需要的资源补充的加载。具体配置也是conf/catalina.properties中,里面的server.loader里指定了该classloader加载的jar和目录。tomcat默认未配置该值。
对于当前3个tomcat自有的classloader的创建过程,我们可以看下图的具体的org.apache.catalina.startup.Bootstrap类。tomcat初始化时会默认初始化这3个classloader。具体逻辑也很清晰。
图3
WebappClassLoaderBase(web应用的类加载器)(该tomcat版本为tomcat_8_5_50,tomat7没有WebappClassLoaderBase,直接搜WebappClassLoader的即可。):tomcat加载应用的核心类加载器,可以查看web容器启动的过程中,对于tomcat的应用隔离主要是每个应用都存在一份WebAppClassLoader,因此WebAppClassLoader的创建工作也是在Context(上下文)中进行的,当前方法可以查看org.apache.catalina.core.StandardContext类.整个web容器生命周期启动时,会调用该startInternal()方法,当当前的classloader为null时,会创建一个WebappClassLoader用于加载资源。而WebappClassLoader的启动可以查看下图中的start()方法。会将/WEB-INF/classes目录下的资源和/WEB-INF/lib下的jar的资源全部合并到一起去,交由WebappClassLoaderBase加载。
图4
图5
而我们知道要想加载自定义资源,那么自有的classloader需要重写loadclass()方法。 那么我们来看下面代码块中webappclassloaderbase的loadclass()实现,以及他是否遵循双亲委派模型。在这里就不得不提之前文章也提到的delegate的概念,tomcat配置delegate的方式在tomcat中配置delegate为true即可具体配置在tomcat的context.xml中配置<Loader delegate="true" />即可,即向上委托,所有的类优先交由上层的classloader去加载。这就是我们解决当应用中的jar和tomcat的jar冲突时的一种手段。
双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。
所以tomcat设计强大之处,可以自由实现应用的当前类,由哪个classloader优先加载,tomcat的应用之间由于相互隔离,所以classes和lib的资源默认由当前WebappClassLoader加载,所以tomcat也没有完全遵循双亲委派模型。
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;
checkStateForClassLoading(name);
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
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);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
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
}
}
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name, true);
// 当delegate配置为true,将委托给父级classloader优先加载,当父级classloader加载不
//到时才会继续交由当前classloader加载。
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
}
}
// 查看本地资源
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
}
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);
}
问题1:如果tomcat 的 CommonClassLoader 想加载 WebAppClassLoader 中的类,怎么解决?
这个我们可以查看双亲委派模型被打破的案例,第一种很显然就是重写loadClass()方法即可。第二种就是我们可以使用线程上下文类加载器实现,使用线程上下文加载器,可以让父类加载器请求子类加载器去完成类加载的动作。
问题2:ExtensionClassLoader中的parent是null,那么为什么他会和bootstrapClassLoader建立了联系?
对于这个问题的解释:有兴趣的可以看ClassLoader的源代码:下面截图是对于这个问题的解释。在classloader中,当加载类时,发现当前classloader的parent classloader为null,那么jvm默认认为他就去寻找根classloader即bootstrapClassloader具体可以findBootstrapClssOrNull(name),查看native的上面注释可知,如果没找到即返回null,再交由下面去加载。这也是常说的双亲委派模型的实现:如图6。
图6
图7
补充说明:
SecureClassLoader 此类来自于java.security包,扩展了ClassLoader,并为定义具有相关代码源和权限的类提供了额外支持,这些代码源和权限默认情况下由系统策略检索。
URLClassLoader 此类来自于java.net包,扩展了SecureClassLoader,该类加载器用于从指向 JAR 文件和目录的 URL 的搜索路径加载类和资源。这里假定任何以 '/' 结束的 URL 都是指向目录的。如果不是以该字符结束,则认为该 URL 指向一个将根据需要打开的 JAR 文件。
图8
图9
后记:classloader 作为jvm的重要一环,需要理解还需要多去扒JDK以及tomcat的源码,在调试和阅读过程中才能更好的理解classloader的真正含义。希望和大家一起进步,走的更高更远!