tomcat类加载机制
Posted 知识拾荒者
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了tomcat类加载机制相关的知识,希望对你有一定的参考价值。
1 jvm类加载器
1.1 ClassLoader介绍
Java的类加载,就是把字节码格式“.class”文件加载到JVM的方法区,并在JVM的堆区建立一个java.lang.Class
对象的实例,用来封装Java类相关的数据和方法。JVM并不是在启动时就把所有的“.class”文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。JVM类加载是由类加载器来完成的,JDK提供一个抽象类ClassLoader,这个抽象类中定义了三个关键方法,分别是loadClass、findClass、defineClass。
public abstract class ClassLoader
//每个类加载器都有个父加载器
private final ClassLoader parent;
public Class loadClass(String name)
//查找一下这个类是不是已经加载过了
Class c = findLoadedClass(name);
//如果没有加载过
if( c == null )
//先委托给父加载器去加载,注意这是个递归调用
if (parent != null)
c = parent.loadClass(name);
else
// 如果父加载器为空,查找Bootstrap加载器是不是加载过了
c = findBootstrapClassOrNull(name);
// 如果父加载器没加载成功,调用自己的findClass去加载
if (c == null)
c = findClass(name);
return c;
protected Class findClass(String name)
//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
...
//2. 调用defineClass将字节数组转成Class对象
return defineClass(buf, off, len);
// 将字节码数组解析成一个Class对象,用native方法实现
protected final Class defineClass(byte[] b, int off, int len)
...
- defineClass是个工具方法,它的职责是调用native方法把Java类的字节码解析成一个Class对象,所谓的native方法就是由C语言实现的方法,Java通过JNI机制调用。
- findClass方法的主要职责就是找到“.class”文件,可能来自文件系统或者网络,找到后把“.class”文件读到内存得到字节码数组,然后调用defineClass方法得到Class对象。
- loadClass是个public方法,说明它才是对外提供服务的接口,具体实现也比较清晰:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。请你注意,这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个Java类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索Java类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载,这就是双亲委托机制。
1.2 JDK类加载器
jdk有三个类加载器,另外你还可以自定义类加载器
- BootstrapClassLoader是启动类加载器,由C语言实现,用来加载JVM启动时所需要的核心类,比如
rt.jar
、resources.jar
等。 - ExtClassLoader是扩展类加载器,用来加载
jrelibext
目录下JAR包。 - AppClassLoader是系统类加载器,用来加载classpath下的类,应用程序默认用它来加载类。
- 自定义类加载器,用来加载自定义路径下的类。
1.3 为什么要使用双亲委托机制?
双亲委托机制是为了保证一个Java类在JVM中是唯一的,假如你不小心写了一个与JRE核心类同名的类,比如Object类,双亲委托机制能保证加载的是JRE里的那个Object类,而不是你写的Object类。这是因为AppClassLoader在加载你的Object类时,会委托给ExtClassLoader去加载,而ExtClassLoader又会委托给BootstrapClassLoader,BootstrapClassLoader发现自己已经加载过了Object类,会直接返回,不会去加载你写的Object类。
2 Tomcat类加载器
Tomcat的自定义类加载器WebAppClassLoader打破了双亲委托机制,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载Web应用自己定义的类。具体实现就是重写ClassLoader的两个方法:findClass和loadClass。
2.1 findClass方法
public Class findClass(String name) throws ClassNotFoundException
...
Class clazz = null;
try
//1. 先在Web应用目录下查找类
clazz = findClassInternal(name);
catch (RuntimeException e)
throw e;
if (clazz == null)
try
//2. 如果在本地目录没有找到,交给父加载器去查找
clazz = super.findClass(name);
catch (RuntimeException e)
throw e;
//3. 如果父类也没找到,抛出ClassNotFoundException
if (clazz == null)
throw new ClassNotFoundException(name);
return clazz;
在findClass方法里,主要有三个步骤:
- 先在Web应用本地目录下查找要加载的类。
- 如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器AppClassLoader。
- 如何父加载器也没找到这个类,抛出ClassNotFound异常。
2.2 loadClass方法
public Class loadClass(String name, boolean resolve) throws ClassNotFoundException
synchronized (getClassLoadingLock(name))
Class clazz = null;
//1. 先在本地cache查找该类是否已经加载过
clazz = findLoadedClass0(name);
if (clazz != null)
if (resolve)
resolveClass(clazz);
return clazz;
//2. 从系统类加载器的cache中查找是否加载过
clazz = findLoadedClass(name);
if (clazz != null)
if (resolve)
resolveClass(clazz);
return clazz;
// 3. 尝试用ExtClassLoader类加载器类加载,为什么?
ClassLoader javaseLoader = getJavaseClassLoader();
try
clazz = javaseLoader.loadClass(name);
if (clazz != null)
if (resolve)
resolveClass(clazz);
return clazz;
catch (ClassNotFoundException e)
// Ignore
// 4. 尝试在本地目录搜索class并加载
try
clazz = findClass(name);
if (clazz != null)
if (resolve)
resolveClass(clazz);
return clazz;
catch (ClassNotFoundException e)
// Ignore
// 5. 尝试用系统类加载器(也就是AppClassLoader)来加载
try
clazz = Class.forName(name, false, parent);
if (clazz != null)
if (resolve)
resolveClass(clazz);
return clazz;
catch (ClassNotFoundException e)
// Ignore
//6. 上述过程都加载失败,抛出异常
throw new ClassNotFoundException(name);
loadClass方法主要有六个步骤:
- 先在本地Cache查找该类是否已经加载过,也就是说Tomcat的类加载器是否已经加载过这个类。
- 如果Tomcat类加载器没有加载过这个类,再看看系统类加载器是否加载过。
- 如果都没有,就让ExtClassLoader去加载,这一步比较关键,目的防止Web应用自己的类覆盖JRE的核心类。因为Tomcat需要打破双亲委托机制,假如Web应用里自定义了一个叫Object的类,如果先加载这个Object类,就会覆盖JRE里面的那个Object类,这就是为什么Tomcat的类加载器会优先尝试用ExtClassLoader去加载,因为ExtClassLoader会委托给BootstrapClassLoader去加载,BootstrapClassLoader发现自己已经加载了Object类,直接返回给Tomcat的类加载器,这样Tomcat的类加载器就不会去加载Web应用下的Object类了,也就避免了覆盖JRE核心类的问题。
- 如果ExtClassLoader加载器加载失败,也就是说JRE核心类中没有这类,那么就在本地Web应用目录下查找并加载。
- 如果本地目录下没有这个类,说明不是Web应用自己定义的类,那么由系统类加载器去加载。这里请你注意,Web应用是通过
Class.forName
调用交给系统类加载器的,因为Class.forName
的默认加载器就是系统类加载器。 - 如果上述加载过程全部失败,抛出ClassNotFound异常。
3 Tomcat类加载器的层次结构
3.1 tomcat类加载器需要解决的问题
Tomcat作为Servlet容器,它负责加载我们的Servlet类,此外它还负责加载Servlet所依赖的JAR包。并且Tomcat本身也是一个Java程序,因此它需要加载自己的类和依赖的JAR包。至少需要思考这几个问题:
- 假如我们在Tomcat中运行了两个Web应用程序,两个Web应用中有同名的Servlet,但是功能不同,Tomcat需要同时加载和管理这两个同名的Servlet类,保证它们不会冲突,因此Web应用之间的类需要隔离。
- 假如两个Web应用都依赖同一个第三方的JAR包,比如Spring,那Spring的JAR包被加载到内存后,Tomcat要保证这两个Web应用能够共享,也就是说Spring的JAR包只被加载一次,否则随着依赖的第三方JAR包增多,JVM的内存会膨胀。
- 跟JVM一样,我们需要隔离Tomcat本身的类和Web应用的类。
3.2 Tomcat类加载器的层次结构
我们先来看第1个问题,假如我们使用JVM默认AppClassLoader来加载Web应用,AppClassLoader只能加载一个Servlet类,在加载第二个同名Servlet类时,AppClassLoader会返回第一个Servlet类的Class实例,这是因为在AppClassLoader看来,同名的Servlet类只被加载一次。
因此Tomcat的解决方案是自定义一个类加载器WebAppClassLoader, 并且给每个Web应用创建一个类加载器实例。我们知道,Context容器组件对应一个Web应用,因此,每个Context容器负责创建和维护一个WebAppClassLoader加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在Java虚拟机内部创建了一个个相互隔离的Java类空间,每一个Web应用都有自己的类空间,Web应用之间通过各自的类加载器互相隔离。
SharedClassLoader
我们再来看第2个问题,本质需求是两个Web应用之间怎么共享库类,并且不能重复加载相同的类。我们知道,在双亲委托机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗,应用程序也正是通过这种方式共享JRE的核心类。因此Tomcat的设计者又加了一个类加载器SharedClassLoader,作为WebAppClassLoader的父加载器,专门来加载Web应用之间共享的类。如果WebAppClassLoader自己没有加载到某个类,就会委托父加载器SharedClassLoader去加载这个类,SharedClassLoader会在指定目录下加载共享类,之后返回给WebAppClassLoader,这样共享的问题就解决了。
CatalinaClassLoader
我们来看第3个问题,如何隔离Tomcat本身的类和Web应用的类?我们知道,要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。基于此Tomcat又设计一个类加载器CatalinaClassLoader,专门来加载Tomcat自身的类。这样设计有个问题,那Tomcat和各Web应用之间需要共享一些类时该怎么办呢?
CommonClassLoader
老办法,还是再增加一个CommonClassLoader,作为CatalinaClassLoader和SharedClassLoader的父加载器。CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader 使用,而CatalinaClassLoader和SharedClassLoader能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
4 spring的加载问题
在JVM的实现中有一条隐含的规则,默认情况下,如果一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载。比如Spring作为一个Bean工厂,它需要创建业务类的实例,并且在创建业务类实例之前需要加载这些类。Spring是通过调用Class.forName
来加载业务类的,我们来看一下forName的源码:
public static Class forName(String className)
Class caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
可以看到在forName的函数里,会用调用者也就是Spring的加载器去加载业务类。
我在前面提到,Web应用之间共享的JAR包可以交给SharedClassLoader来加载,从而避免重复加载。Spring作为共享的第三方JAR包,它本身是由SharedClassLoader来加载的,Spring又要去加载业务类,按照前面那条规则,加载Spring的类加载器也会用来加载业务类,但是业务类在Web应用目录下,不在SharedClassLoader的加载路径下,这该怎么办呢?
于是线程上下文加载器登场了,它其实是一种类加载器传递机制。为什么叫作“线程上下文加载器”呢,因为这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。因此Tomcat为每个Web应用创建一个WebAppClassLoader类加载器,并在启动Web应用的线程里设置线程上下文加载器,这样Spring在启动时就将线程上下文加载器取出来,用来加载Bean。Spring取线程上下文加载的代码如下:
cl = Thread.currentThread().getContextClassLoader();
在StandardContext的启动方法里,会将当前线程的上下文加载器设置为WebAppClassLoader。
originalClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(webApplicationClassLoader);
在启动方法结束的时候,还会恢复线程的上下文加载器:
Thread.currentThread().setContextClassLoader(originalClassLoader);
以上是关于tomcat类加载机制的主要内容,如果未能解决你的问题,请参考以下文章