我们要分析清楚Tomcat中的类加载器相关的内容之前我们还是需要把JVM中的类加载器给大家理清楚。
类加载器的作用就是从文件系统或者网络中加载Class文件,至于他是否可以运行就不是ClassLoader的工作了。
JVM中支持的类加载器有两种类型,分别是 引导类加载器
【Bootstrap ClassLoader】和 自定义类加载器
【User-Defined ClassLoader】
在Java虚拟机层面定义:所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
可以通过源码看到对应的类加载器的继承关系
ExtClassLoader
AppClassLoader
通过具体的案例代码可以来看看类加载器的使用
public static void main(String[] args) {// 获取系统类加载器ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println("systemClassLoader = " + systemClassLoader);// 获取父类加载器ClassLoader parent = systemClassLoader.getParent();System.out.println("parent = " + parent);// 继续获取上层的类加载器ClassLoader bootstrapClassLoader = parent.getParent();System.out.println("bootstrapClassLoader = " + bootstrapClassLoader);// 自定义Java类ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();System.out.println("classLoader = " + classLoader);// Java系统类ClassLoader classLoader1 = String.class.getClassLoader();System.out.println("classLoader1 = " + classLoader1);}
对应的输出结果
systemClassLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
parent = sun.misc.Launcher$ExtClassLoader@61bbe9ba
bootstrapClassLoader = null
classLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
classLoader1 = null
虚拟机自带的类加载器,启动类加载器
通过代码来看看具体的加载路径有哪些
public static void main(String[] args) {System.out.println("**************启动类加载器*************");URL[] urLs &#61; Launcher.getBootstrapClassPath().getURLs();for (int i &#61; 0 ; i < urLs.length ; i &#43;&#43;){URL urL &#61; urLs[i];System.out.println("urL &#61; " &#43; urL.toExternalForm());}}
输出结果&#xff1a;
**************启动类加载器*************
urL &#61; file:/D:/software/java/jdk8/jre/lib/resources.jar
urL &#61; file:/D:/software/java/jdk8/jre/lib/rt.jar
urL &#61; file:/D:/software/java/jdk8/jre/lib/sunrsasign.jar
urL &#61; file:/D:/software/java/jdk8/jre/lib/jsse.jar
urL &#61; file:/D:/software/java/jdk8/jre/lib/jce.jar
urL &#61; file:/D:/software/java/jdk8/jre/lib/charsets.jar
urL &#61; file:/D:/software/java/jdk8/jre/lib/jfr.jar
urL &#61; file:/D:/software/java/jdk8/jre/classes
虚拟机自带的加载器。扩展类加载器&#xff0c;Java语音编写&#xff0c;是sun.misc.Launcher的内部类
派生于ClassLoader所以是自定义类加载器
父类加载器是BootstrapClassLoader。
扩展类加载器是从扩展目录 java.ext.dirs
系统属性指定的目录中加载类库&#xff0c;或者从JDK的安装目录的 jre/lib/ext
子目录下加载类库&#xff0c;如果用户创建的jar包也放在了这个目录下&#xff0c;那么该类加载器也会自动加载的。
然后通过案例来看看扩展类加载器加载的路径
public static void main(String[] args) {System.out.println("**************扩展类加载器*************");String extDirs &#61; System.getProperty("java.ext.dirs");System.out.println("extDirs &#61; " &#43; extDirs);}
对应的输出结果
**************扩展类加载器*************
extDirs &#61; D:\software\java\jdk8\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
然后通过加载路径下的Java类测试也能证明
虚拟机自带的加载器&#xff0c;APPClassLoader也叫应用程序类加载器。Java语音编写&#xff0c;由sun.misc.Launcher下的内部类提供。也是派生于ClassLoader。
父类加载器为ExtClassLoader
主要负责加载环境变量classpath或系统属性 java.class.path 指定路径下的类库&#xff0c;该类加载器是程序中的默认类加载器&#xff0c;一般来说&#xff0c;Java应用的类都是由它来完成加载的。通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。
在我们的日常应用程序的开发中&#xff0c;我基本是用不到自定义类加载器的&#xff0c;基本就是由前面介绍的这三个类加载器来相互配合搞定的。但是在有些特殊的情况下我们不希望通过系统的类加载器来处理&#xff0c;这时我们就可以通过自定义类加载来实现了。
用户自定义类加载器的实现步骤&#xff1a;
Java虚拟机对class文件采用的是
按需加载
的方式&#xff0c;也就是当需要使用该类时才会将他的class文件加载到内存中生成class对象&#xff0c;而且加载某个类的class文件时&#xff0c;Java虚拟机采用的而是双亲委派模式。即把请求交由父类加载器处理&#xff0c;它是一种任务委派模式。
双亲委派的工作原理&#xff1a;
举个简单的例子&#xff0c;我们自定义一个java.lang.String类&#xff0c;然后添加对应的main方式执行。
public class String {public static void main(String[] args) {System.out.println("自定义String类");}
}
报错信息为&#xff1a;
原因就是双亲委派机制通过BootstrapClassLoader加载的是java包下的String&#xff0c;而不会加载我们自定义的。
双亲委派机制的有点&#xff1a;
掌握了Java的类加载器和双亲委派机制&#xff0c;现在我们就可以回答正题上来了&#xff0c;Tomcat的类加载器是怎么设计的&#xff1f;
Web容器有其自身的特殊性&#xff0c;所以在类加载器这块是不能完全使用JVM的类加载器的双亲委派机制的。在Web容器中我们应该要满足如下的特性&#xff1a;
隔离性&#xff1a;
部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离。设想一下&#xff0c;两个Web应用&#xff0c;一个使用了Spring3.0&#xff0c;另一个使用了新的的5.0&#xff0c;应用服务器使用一个类加载器&#xff0c;Web应用将会因为jar包覆盖而无法启动。
灵活性:
Web应用之间的类加载器相互独立&#xff0c;那么就能针对一个Web应用进行重新部署&#xff0c;此时Web应用的类加载器会被重建&#xff0c;而且不会影响其他的Web应用。如果采用一个类加载器&#xff0c;类之间的依赖是杂乱复杂的&#xff0c;无法完全移出某个应用的类。
性能:
性能也是一个比较重要的点。部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以互相共享。这个需求也很常见&#xff0c;例如&#xff0c;用户可能有10个使用Spring框架的应用程序部署在同一台服务器上&#xff0c;如果把10份Spring分别存放在各个应用程序的隔离目录中&#xff0c;将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题&#xff0c;而是指类库在使用时都要被加载到Web容器的内存&#xff0c;如果类库不能共享&#xff0c;虚拟机的方法区就会很容易出现过度膨胀的风险。
明白了Web容器的类加载器有多个&#xff0c;再来看tomcat的类加载器结构。
首先上张图&#xff0c;整体看下tomcat的类加载器&#xff1a;
可以看到在原先的java类加载器基础上&#xff0c;tomcat新增了几个类加载器&#xff0c;包括3个基础类加载器和每个Web应用的类加载器&#xff0c;其中3个基础类加载器可在conf/catalina.properties中配置&#xff0c;具体介绍下&#xff1a;
Common&#xff1a;以应用类加载器为父类&#xff0c;是tomcat顶层的公用类加载器&#xff0c;其路径由conf/catalina.properties中的common.loader指定&#xff0c;默认指向${catalina.base}/lib下的包。
Catalina&#xff1a;以Common类加载器为父类&#xff0c;是用于加载Tomcat应用服务器的类加载器&#xff0c;其路径由server.loader指定&#xff0c;默认为空&#xff0c;此时tomcat使用Common类加载器加载应用服务器。
Shared&#xff1a;以Common类加载器为父类&#xff0c;是所有Web应用的父类加载器&#xff0c;其路径由shared.loader指定&#xff0c;默认为空&#xff0c;此时tomcat使用Common类加载器作为Web应用的父加载器。
Web应用&#xff1a;以Shared类加载器为父类&#xff0c;加载/WEB-INF/classes目录下的未压缩的Class和资源文件以及/WEB-INF/lib目录下的jar包&#xff0c;该类加载器只对当前Web应用可见&#xff0c;对其他Web应用均不可见。
默认情况下&#xff0c;Common、Catalina、Shared类加载器是同一个&#xff0c;但可以配置3个不同的类加载器&#xff0c;使他们各司其职。
首先&#xff0c;Common类加载器复杂加载Tomcat应用服务器内部和Web应用均可见的类&#xff0c;如Servlet规范相关包和一些通用工具包。
其次&#xff0c;Catalina类加载器负责只有Tomcat应用服务器内部可见的类&#xff0c;这些类对Web应用不可见。比如&#xff0c;想实现自己的会话存储方案&#xff0c;而且该方案依赖了一些第三方包&#xff0c;当然是不希望这些包对Web应用可见&#xff0c;这时可以配置server.load&#xff0c;创建独立的Catalina类加载器。
再次&#xff0c;Shared类复杂加载Web应用共享类&#xff0c;这些类tomcat服务器不会依赖
首先来看看Tomcat的类加载器的继承结构
可以看到继承的结构和我们上面所写的类加载器的结构不同。
大家需要注意双亲委派机制并不是通过继承来实现的&#xff0c;而是相互之间组合而形成的。所以AppClassLoader没有继承自 ExtClassLoader&#xff0c;WebappClassLoader也没有继承自AppClassLoader。至于Common ClassLoader &#xff0c;Shared ClassLoader&#xff0c;Catalina ClassLoader则是在启动时初始化的三个不同名字的URLClassLoader。
先来看看Bootstrap#init()方法。init方法会调用initClassLoaders&#xff0c;同样也会将Catalina ClassLoader设置到当前线程设置到当前线程&#xff0c;进入initClassLoaders来看看。
private void initClassLoaders() {try {// 创建 commonLoader catalinaLoader sharedLoadercommonLoader &#61; createClassLoader("common", null);if (commonLoader &#61;&#61; null) {// no config file, default to this loader - we might be in a &#39;single&#39; env.commonLoader &#61; this.getClass().getClassLoader();}// 默认情况下 server.loader 和 shared.loader 都为空则会返回 commonLoader 类加载器catalinaLoader &#61; createClassLoader("server", commonLoader);sharedLoader &#61; createClassLoader("shared", commonLoader);} catch (Throwable t) {handleThrowable(t);log.error("Class loader creation threw exception", t);System.exit(1);}}
我们可以看到在initClassLoaders()方法中完成了CommonClassLoader&#xff0c; CatalinaClassLoader&#xff0c;和SharedClassLoader的创建&#xff0c;而且进入到createClassLoader方法中。
可以看到这三个基础类加载器所加载的资源刚好对应conf/catalina.properties中的common.loader&#xff0c;server.loader&#xff0c;shared.loader
Common/Catalina/Shared ClassLoader的创建好了之后就会维护相互之间的组合关系
其实也就是设置了父加载器
源码比较长&#xff0c;直接进入到 WebappClassLoaderBase
中的 LoadClass方法
&#64;Overridepublic Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {if (log.isDebugEnabled()) {log.debug("loadClass(" &#43; name &#43; ", " &#43; resolve &#43; ")");}Class<?> clazz &#61; null;// Log access to stopped class loadercheckStateForClassLoading(name);// (0) Check our previously loaded local class cache// 检查WebappClassLoader中是否加载过此类clazz &#61; findLoadedClass0(name);if (clazz !&#61; null) {if (log.isDebugEnabled()) {log.debug(" Returning class from cache");}if (resolve) {resolveClass(clazz);}return clazz;}// (0.1) Check our previously loaded class cache// 如果第一步没有找到&#xff0c;则继续检查JVM虚拟机中是否加载过该类clazz &#61; findLoadedClass(name);if (clazz !&#61; null) {if (log.isDebugEnabled()) {log.debug(" Returning class from cache");}if (resolve) {resolveClass(clazz);}return clazz;}// (0.2) Try loading the class with the bootstrap class loader, to prevent// the webapp from overriding Java SE classes. This implements// SRV.10.7.2// 如果前两步都没有找到&#xff0c;则使用系统类加载该类&#xff08;也就是当前JVM的ClassPath&#xff09;。// 为了防止覆盖基础类实现&#xff0c;这里会判断class是不是JVMSE中的基础类库中类。String resourceName &#61; binaryNameToPath(name, false);ClassLoader javaseLoader &#61; getJavaseClassLoader();boolean tryLoadingFromJavaseLoader;try {// Use getResource as it won&#39;t trigger an expensive// ClassNotFoundException if the resource is not available from// the Java SE class loader. However (see// https://bz.apache.org/bugzilla/show_bug.cgi?id&#61;58125 for// details) when running under a security manager in rare cases// this call may trigger a ClassCircularityError.// See https://bz.apache.org/bugzilla/show_bug.cgi?id&#61;61424 for// details of how this may trigger a StackOverflowError// Given these reported errors, catch Throwable to ensure any// other edge cases are also caughtURL url;if (securityManager !&#61; null) {PrivilegedAction<URL> dp &#61; new PrivilegedJavaseGetResource(resourceName);url &#61; AccessController.doPrivileged(dp);} else {url &#61; javaseLoader.getResource(resourceName);}tryLoadingFromJavaseLoader &#61; (url !&#61; null);} catch (Throwable t) {// Swallow all exceptions apart from those that must be re-thrownExceptionUtils.handleThrowable(t);// The getResource() trick won&#39;t work for this class. We have to// try loading it directly and accept that we might get a// ClassNotFoundException.tryLoadingFromJavaseLoader &#61; true;}if (tryLoadingFromJavaseLoader) {try {clazz &#61; javaseLoader.loadClass(name);if (clazz !&#61; null) {if (resolve) {resolveClass(clazz);}return clazz;}} catch (ClassNotFoundException e) {// Ignore}}// (0.5) Permission to access this class when using a SecurityManagerif (securityManager !&#61; null) {int i &#61; name.lastIndexOf(&#39;.&#39;);if (i >&#61; 0) {try {securityManager.checkPackageAccess(name.substring(0,i));} catch (SecurityException se) {String error &#61; sm.getString("webappClassLoader.restrictedPackage", name);log.info(error, se);throw new ClassNotFoundException(error, se);}}}// 检查是否 设置了delegate属性&#xff0c;设置为true&#xff0c;那么就会完全按照JVM的"双亲委托"机制流程加载类。boolean delegateLoad &#61; delegate || filter(name, true);// (1) Delegate to our parent if requestedif (delegateLoad) {if (log.isDebugEnabled()) {log.debug(" Delegating to parent classloader1 " &#43; parent);}try {clazz &#61; Class.forName(name, false, parent);if (clazz !&#61; null) {if (log.isDebugEnabled()) {log.debug(" Loading class from parent");}if (resolve) {resolveClass(clazz);}return clazz;}} catch (ClassNotFoundException e) {// Ignore}}// (2) Search local repositories// 若是没有委托&#xff0c;则默认会首次使用WebappClassLoader来加载类。通过自定义findClass定义处理类加载规则。// findClass()会去Web-INF/classes 目录下查找类。if (log.isDebugEnabled()) {log.debug(" Searching local repositories");}try {clazz &#61; findClass(name);if (clazz !&#61; null) {if (log.isDebugEnabled()) {log.debug(" Loading class from local repository");}if (resolve) {resolveClass(clazz);}return clazz;}} catch (ClassNotFoundException e) {// Ignore}// (3) Delegate to parent unconditionally// 若是WebappClassLoader在/WEB-INF/classes、/WEB-INF/lib下还是查找不到class&#xff0c;// 那么无条件强制委托给System、Common类加载器去查找该类。if (!delegateLoad) {if (log.isDebugEnabled()) {log.debug(" Delegating to parent classloader at end: " &#43; parent);}try {clazz &#61; Class.forName(name, false, parent);if (clazz !&#61; 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应用类加载器默认的加载顺序是&#xff1a;
(1).先从缓存中加载&#xff1b;
(2).如果没有&#xff0c;则从JVM的Bootstrap类加载器加载&#xff1b;
(3).如果没有&#xff0c;则从当前类加载器加载&#xff08;按照WEB-INF/classes、WEB-INF/lib的顺序&#xff09;&#xff1b;
(4).如果没有&#xff0c;则从父类加载器加载&#xff0c;由于父类加载器采用默认的委派模式&#xff0c;所以加载顺序是AppClassLoader、Common、Shared。
tomcat提供了delegate属性用于控制是否启用java委派模式&#xff0c;默认false&#xff08;不启用&#xff09;&#xff0c;当设置为true时&#xff0c;tomcat将使用java的默认委派模式&#xff0c;这时加载顺序如下&#xff1a;
(1).先从缓存中加载&#xff1b;
(2).如果没有&#xff0c;则从JVM的Bootstrap类加载器加载&#xff1b;
(3).如果没有&#xff0c;则从父类加载器加载&#xff0c;加载顺序是AppClassLoader、Common、Shared。
(4).如果没有&#xff0c;则从当前类加载器加载&#xff08;按照WEB-INF/classes、WEB-INF/lib的顺序&#xff09;&#xff1b;