Tomcat类加载机制分析
JDK的类加载机制是双亲委派:先委托给他的parent类加载器,让parent类加载器先来加载,如果没有,才再在自己的路径上加载。
但是Tomcat的类加载机制与此不同。
Tomcat在初始化时,会创建以下4种类加载器(Tomcat 8.5)。
Bootstrap
|
System
|
Common
/ \
Webapp1 Webapp2 ...
- Bootstrap:负责加载JVM提供的基础的运行时类(即
rt.jar)以及${JAVA\_HOME}/jre/lib/ext下的类(相当于JDK本身的Bootstrap和Extension这两个类加载器的功能)。 - System:该类加载器通常会加载环境变量CLASSPATH中的类,但是Tomcat并不是如此,它会忽略CLASSPATH,而去加载
$CATALINA_HOME/bin目录下的3个jar(启动脚本中写死的):bootstrap.jartomcat-juli.jar(如果$CATALINA_BASE/bin目录下也有这个包,会使用$CATALINA_BASE/bin目录下的)commons-daemon.jar
- Common :该类加载器加载的类,对Tomcat自身和所有Web应用都可见。通常情况下,应用的类文件不应该放在Common ClassLoader中。Common会扫描
$CATALINA_BASE/conf/catalina.properties中common.loader属性指定的路径中的类文件。默认情况下,它会按顺序去下列路径中去加载:$CATALINA_BASE/lib中未打包的classes和resources$CATALINA_BASE/lib中的jar文件$CATALINA_HOME/lib中未打包的classes和resources$CATALINA_HOME/lib中的jar文件
默认情况下,这些路径下的jar主要有以下这些:
- annotations-api.jar — JavaEE annotations classes.
- catalina.jar — Implementation of the Catalina servlet container portion of Tomcat.
- catalina-ant.jar — Tomcat Catalina Ant tasks.
- catalina-ha.jar — High availability package.
- catalina-storeconfig.jar — Generation of XML configuration files from current state
- catalina-tribes.jar — Group communication package.
- ecj-*.jar — Eclipse JDT Java compiler.
- el-api.jar — EL 3.0 API.
- jasper.jar — Tomcat Jasper JSP Compiler and Runtime.
- jasper-el.jar — Tomcat Jasper EL implementation.
- jsp-api.jar — JSP 2.3 API.
- servlet-api.jar — Servlet 3.1 API.
- tomcat-api.jar — Several interfaces defined by Tomcat.
- tomcat-coyote.jar — Tomcat connectors and utility classes.
- tomcat-dbcp.jar — Database connection pool implementation based on package-renamed copy of Apache Commons Pool and Apache Commons DBCP.
- tomcat-i18n-**.jar — Optional JARs containing resource bundles for other languages. As default bundles are also included in each individual JAR, they can be safely removed if no internationalization of messages is needed.
- tomcat-jdbc.jar — An alternative database connection pool implementation, known as Tomcat JDBC pool. See documentation for more details.
- tomcat-util.jar — Common classes used by various components of Apache Tomcat.
- tomcat-websocket.jar — WebSocket 1.1 implementation
- websocket-api.jar — WebSocket 1.1 API
- WebappX 每一个部署在Tomcat中的web应用,Tomcat都会为其创建一个WebappClassloader,它会去加载应用
WEB-INF/classes目录下所有未打包的classes和resources,然后再去加载WEB-INF/lib目录下的所有jar文件。每个应用的WebappClassloader都不同,因此,它加载的类只对本应用可见,其他应用不可见(这是实现web应用隔离的关键)。
正如上面提到的,WebappClassloader的行为与Java默认的双亲委派模型是有所区别的。当WebappClassloader收到加载类的请求时,它首先在自己的路径(repository)中去寻找类的class文件(而不是委托给父类去加载)。但是有例外:JRE中的类库是不允许重写的!
从应用的视角来看,当有类加载的请求时,class或者resource的查找顺序是这样的:
JVM中的类库,如
rt.jar和$JAVA_HOME/jre/lib/ext目录下的jar应用的
/WEB-INF/classes目录
应用的
/WEB-INF/lib/*.jarSystemClassloader加载的类(如上所述)CommonClassloader加载的类(如上所述)
但是,如果你的应用配置了<loader delegate="true"/>,那么查找顺序就会变为:
- JVM中的类库,如
rt.jar和$JAVA_HOME/jre/lib/ext目录下的jar
SystemClassloader加载的类(如上所述)CommonClassloader加载的类(如上所述)应用的
/WEB-INF/classes目录应用的
/WEB-INF/lib/*.jar
QA
Q:默认配置下,在项目中如果在一个Tomcat内部署多个应用,甚至多个应用内使用了某个类似的几个不同版本,但它们之间却互不影响。这是如何做到的?
A:多个应用之间的相同类库,由于使用不同的类加载器进行加载,它们仍然是不同的对象。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性:比较两个类是否“相等”,只有在这两个类是由同一个类加载器的前提下才有意义,否则即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。这里所指的相等,包括代表类的Class类的对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。
Q:如果多个应用都用到了某类似的相同版本,是否可以统一提供,不在各个应用内分别提供,占用内存?
A:可以,可以将这些公共的类库放在Common类加载器的扫描范围,具体做法是在
$CATALINA_BASE/conf/catalina.properties中common.loader属性中追加你自己的应用共用的jar。common.loader默认值为:common.loader="${catalina.base}/lib", "${catalina.base}/lib/*.jar", "${catalina.home}/lib", "${catalina.home}/lib/*.jar"
Q:在开发Web应用时,在pom.xml中添加了servlet-api的依赖,那实际应用的class加载时,会加载应用本身引入的servlet-api这个jar吗?
A:不会。对于我们应用内提供的Servlet-api,应用服务器是不会加载的,因为容器已经自已加载过了。这里不是因为父优先还是子优先的问题,而是这类内容,是不允许被重写的。如果你应用内有一个叫javax.servlet.Servlet的class,那加载后可能就影响了应用内的正常运行了。
Q:假如有一个jar包(非Java规范或Servlet规范的实现包,只是普通的其他第三方包),这个jar文件在$CATALINA_HOME/bin(或者在$CATALINA_HOME/lib)目录、应用本身的/WEB-INF/lib目录下都各有一份,那么最终应用中使用的是哪个jar?
A:分为两种情况讨论
- 1.Jar在
$CATALINA_HOME/bin和WEB-INF/lib下 这种情况下,总是以应用中的jar为准:因为自己放在$CATALINA_HOME/bin下的jar根本不会被加载,SystemClassloader只会加载$CATALINE_HOME/bin下的自带的3个jar(上面提高的那三个),这是在Tomcat中的启动脚本中catalina.sh写死的,其他放在该目录的jar,不会被加载。- 2.Jar在
$CATALINA_HOME/lib和WEB-INF/lib下
这种情况下,又有两种情形:①默认情形(<loader delegate="false">),按照Tomcat的类加载机制,WebappClassloader加载,而WebappClassLoader首先在自己的路径范围(即/WEB-INF/lib目录)查找,发现自己的路径内含有要加载的类的class文件,就会将其load到JVM中,结束这次类加载;②修改了Tomcat默认的类加载机制,即配置了<loader delegate=true>,那么WebappClassloader在收到加载jar内某各类的加载请求时,会直接将类加载请求委托给父类CommonClassloader,CommonClassloader就会去自己的路径范围去查找对应class文件,完成加载。验证思路:
①写一个类Dog,打包,将其放在
$CATALINA_HOME/bin或者$CATALINA_HOME/lib下public class Dog{ public Dog(){ System.out.println("a dog born in tomcat internal"); } }②将Dog类该写为,打包,新增webapp项目,将jar放在该webapp项目的
/WEB-INF/lib目录下public class Dog{ public Dog(){ System.out.println("a dog born in webapp"); } }③使该项目在该Tomcat下运行,观察输出内容。
④修改
$CATALINA_HOME/conf/Context.xml,添加内容<Loader delegate="true"/>,再次运行项目,观察输出内容
TODO:这种类加载机制的实现细节。比如,当java.util.ArralyList的加载时的流程走向的实现细节。