Tomcat的类加载器可以分为两部分,第一个是Tomcat自身所使用的类加载器,会加载jre的lib包及tomcat的lib包的类,遵循类加载的双亲委派机制;第二个是每个Web应用程序用的,每个web应用程序都有自己专用的WebappClassLoader,优先加载/web-inf/lib下的jar中的class文件,这样就隔离了每个web应用程序的影响,但是webappClassLoader没有遵循类加载的双亲委派机制,处理的方法就是在使用webappClassLoader的load加载类会进行过滤,如果有些类被过滤掉还是通过双亲委派机制优先从父加载器中加载类。
那么tomcat为什么需要打破双亲委派呢
因为 如下所示的两个图:
多个应用下的话 加载 test类 应用2 加载的test就是应用1 加载过得 而 不是 应用2 自己的
所以每个应用自己搞个类加载器 (因为类名相同啊 jvm区分唯一类 是按照 类加载器 再加上类的全限定名)
只需要写一个类 去生成不同的实例 也就是说 每个应用一个 webappClassLoader实例 (类加载器来实现) set目录 按照目录去管理
一、Tomcat类加载器初始化
在tomcat调用Bootstrap进行启动时会调用initClassLoaders创建3个ClassLoader,它们分别是commonLoader,catalinaLoader,sharedLoader,遵循双亲委派机制。commonLoader会根据tomcat的conf/catalina.properties中的配置加载tomcat自身的jar,然后将这个类加载器作为整个tomcat容器的父类加载器。
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;
private void initClassLoaders() {
try {
// CommonClassLoader是一个公共的类加载器,默认加载${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar下的class
commonLoader = createClassLoader("common", null); // 虽然这个地方parent是null,实际上是appclassloader
// System.out.println("commonLoader的父类加载器===="+commonLoader.getParent());
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
// 下面这个两个类加载器默认情况下就是commonLoader
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
在createClassLoader中会加载tomcat的lib/*.jar下的所有jar中的class文件。
/**
*
* @param name 是配置项的名字,全名为name.loader,配置项配置了类加载器应该从哪些目录去加载类
* @param parent 父级类加载器
* @return
* @throws Exception
*/
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent;
value = replace(value);
List repositories = new ArrayList();
StringTokenizer tokenizer = new StringTokenizer(value, ",");
while (tokenizer.hasMoreElements()) {
String repository = tokenizer.nextToken().trim();
if (repository.length() == 0) {
continue;
}
// Check for a JAR URL repository
try {
// 从URL上获取Jar包资源
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(
new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
// Local repository
if (repository.endsWith("*.jar")) {
// 表示目录下所有的jar包资源
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(
new Repository(repository, RepositoryType.GLOB));
} else if (repository.endsWith(".jar")) {
// 表示目录下当个的jar包资源
repositories.add(
new Repository(repository, RepositoryType.JAR));
} else {
// 表示目录下所有资源,包括jar包、class文件、其他类型资源
repositories.add(
new Repository(repository, RepositoryType.DIR));
}
}
// 基于类仓库类创建一个ClassLoader
return ClassLoaderFactory.createClassLoader(repositories, parent);
}
接下来我们看看这3个类加载器都使用了在什么地方,commonLoader除了在initClassLoader处使用外并没有在其他地方使用,它是作为catalinaLoader和sharedLoader的父类加载器;catalinaLoader在init方法中被设置为当前线程的类加载器。
public void init() throws Exception {
//.....省略部分代码
//将catalinaLoader设置为当前线程的类加载器
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
//.....省略部分代码
}
sharedLoader类加载器作为参数调用了Catalina的setParentClassLoader方法,成为了整个Catalina容器的父类加载器,当然也是WebAppClassLoader的父类加载器。
/**
* Initialize daemon.
* 主要初始化类加载器,在Tomcat的设计中,使用了很多自定义的类加载器,包括Tomcat自己本身的类会由CommonClassLoader来加载,每个wabapp由特定的类加载器来加载
*/
public void init()
throws Exception
{
// Set Catalina path
// catalina.home表示安装目录
// catalina.base表示工作目录
setCatalinaHome();
setCatalinaBase();
// 初始化commonLoader、catalinaLoader、sharedLoader
// 其中catalinaLoader、sharedLoader默认其实就是commonLoader
initClassLoaders();
// 设置线程的所使用的类加载器,默认情况下就是commonLoader
Thread.currentThread().setContextClassLoader(catalinaLoader);
// 如果开启了SecurityManager,那么则要提前加载一些类
SecurityClassLoad.securityClassLoad(catalinaLoader);
// Load our startup class and call its process() method
// 加载Catalina类,并生成instance
if (log.isDebugEnabled())
log.debug("Loading startup class");
Class> startupClass =
catalinaLoader.loadClass
("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.newInstance();
// Set the shared extensions class loader
// 设置Catalina实例的父级类加载器为sharedLoader(默认情况下就是commonLoader)
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
二、WebAppClassLoader应用类加载器
在tomcat中对每个应用都有一个WebAppClassLoader用来隔绝不同应用之前的class文件,因此WebAppClassLoader的创建工作也是在Context中进行的,在StandardContext的startInternal方法开启一个web应用时会创建类加载器。
// 如果没有配,则生成一个WebappLoader
if (getLoader() == null) {
// Webapp类加载器的父类加载器为Host的ParentClassLoader,最终就是Catalina类的类加载器,其实就是CommonClassLoader
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
同时会启动类加载器
// Start our subordinate components, if any
Loader loader = getLoaderInternal(); // 获取Context的类加载器
if ((loader != null) && (loader instanceof Lifecycle))
((Lifecycle) loader).start(); // 启动类加载器,包括初始话DirContext
在WebappClassLoader的父类WebappClassLoaderBase中实现了start方法
/**
* Start the class loader.
*
* @exception LifecycleException if a lifecycle error occurs
*/
@Override
public void start() throws LifecycleException {
started = true;
String encoding = null;
try {
encoding = System.getProperty("file.encoding");
} catch (SecurityException e) {
return;
}
if (encoding.indexOf("EBCDIC")!=-1) {
needConvert = true;
}
for (int i = 0; i < repositories.length; i++) {
if (repositories[i].equals("/WEB-INF/classes/")) {
try {
// 将/WEB-INF/classes/转变成URL对象,并赋值给webInfClassesCodeBase
webInfClassesCodeBase = files[i].toURI().toURL();
} catch (MalformedURLException e) {
// Ignore - leave it as null
}
break;
}
}
}
在WebappClassLoaderBase中重写了ClassLoader的loadClass方法,在这个实现方法中我们可以一窥tomcat真正的类加载机制,简单来说web应用首先还是去尝试加载jre下面的类这个流程是不可变的,接下来web应用就可以根据设置首先是加载自己应用下的class文件还是tomcat的lib目录下的class文件了,实现逻辑看loadClass的实现机制还是比较简单的,所有通过设置web应用可以遵循类加载的双亲委派机制或者不遵循双亲委派机制了。
/**
* Load the class with the specified name, searching using the following
* algorithm until it finds and returns the class. If the class cannot
* be found, returns ClassNotFoundException
.
*
* - Call
findLoadedClass(String)
to check if the
* class has already been loaded. If it has, the same
* Class
object is returned.
* - If the
delegate
property is set to true
,
* call the loadClass()
method of the parent class
* loader, if any.
* - Call
findClass()
to find this class in our locally
* defined repositories.
* - Call the
loadClass()
method of our parent
* class loader, if any.
*
* If the class was found using the above steps, and the
* resolve
flag is true
, this method will then
* call resolveClass(Class)
on the resulting Class object.
*
* @param name Name of the class to be loaded
* @param resolve If true
then resolve the class
*
* @exception ClassNotFoundException if the class was not found
*
* 重写了loadClass方法
* 默认的 loadClass 方法实现了双亲委派机制的逻辑,即会先让父类加载器加载,当无法加载时,才由自己加载。
*/
@SuppressWarnings("sync-override")
@Override
public Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLockInternal(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class> clazz = null;
// Log access to stopped classloader
if (!started) {
try {
throw new IllegalStateException();
} catch (IllegalStateException e) {
log.info(sm.getString("webappClassLoader.stopped", name), e);
}
}
// (0) Check our previously loaded local class cache
// 先检查该类是否已经被Webapp类加载器加载。
clazz = findLoadedClass0(name); // map
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// (0.1) Check our previously loaded class cache
// 该方法直接调用findLoadedClass0本地方法,findLoadedClass0方法会检查JVM缓存中是否加载过此类
clazz = findLoadedClass(name); // jvm 内存
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
// (0.2) Try loading the class with the system class loader, to prevent
// the webapp from overriding J2SE classes
// 尝试通过系统类加载器(AppClassLoader)加载类,防止webapp重写JDK中的类
// 假设,webapp想自己去加载一个java.lang.String的类,这是不允许的,必须在这里进行预防。否则会报错
try {
clazz = j2seClassLoader.loadClass(name); // java.lang.Object
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (0.5) Permission to access this class when using a SecurityManager
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;
if (name.endsWith("BeanInfo")) {
// BZ 57906: suppress logging for calls from
// java.beans.Introspector.findExplicitBeanInfo()
log.debug(error, se);
} else {
log.info(error, se);
}
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name); // 委托--true
// (1) Delegate to our parent if requested
// 是否委派给父类去加载
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
}
}
// (2) Search local repositories
// 从webapp应用内部进行加载
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name); // classes,lib
if (clazz != 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
// 如果webapp应用内部没有加载到类,那么无条件委托给父类进行加载
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);
}
热部署与热加载的区别:
热加载:服务器会监听 class 文件改变,包括web-inf/class,wen-inf/lib,web-inf/web.xml等文件,若发生更改,则局部进行加载,不清空session ,不释放内存。开发中用的多,但是要考虑内存溢出的情况。 重新加载类或jar包,执行容器结构是Context,表示应用。耗时短
热部署: 整个项目从新部署,包括你从新打上.war 文件。 会清空session ,释放内存。项目打包的时候用的多。 执行容器结构是Host,表示主机。 耗时长
热部署和热加载都需要监听相应的文件或文件夹是否发生了变化。它们都是由Tomcat的后台线程触发的。
BackgroundProcessor就表示后台线程。
每个容器都可以拥有一个BackgroundProcessor,但是默认情况下只有Engine容器会在启动的时候启动一个BackgroundProcessor线程。
该线程会每隔一段时间(可以设置,单位为秒),去执行后台任务,先执行本容器定义的后台任务,然后再执行子容器的定义的后台任务,子容器的任务执行完成后会继续执行其子容器的任务,直到没有子容器为止。从这里可以看出就算每个容器自己开启一个BackgroundProcessor,也只不过是多了一个执行相同任务的线程而已,执行任务的效率有所提升。
对于后台任务,所有容器会有一些统一的任务需要执行:
在这个过程中的第2步中会触发热加载,第6步中会触发热部署
执行时机
engine.start()
org.apache.catalina.core.ContainerBase#startInternal
protected void threadStart() {
if (thread != null)
return;
// System.out.println(this.getInfo() + "的backgroundProcessorDelay等于=" + backgroundProcessorDelay);
// 默认情况下只有Engine的backgroundProcessorDelay大于0,为10,
// 也就是说,虽然每个容器在启动的时候都会走到当前方法,但是只有Engine能继续往下面去执行
// 但是其他容器是可以配置backgroundProcessorDelay属性的,只要配置了大于0,那么这个容器也会单独开启一个backgroundProcessor线程
if (backgroundProcessorDelay <= 0)
return;
threadDone = false;
String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
// ContainerBackgroundProcessor线程每隔一段时间会调用容器内的backgroundProcess方法,并且会调用子容器的backgroundProcess方法
thread = new Thread(new ContainerBackgroundProcessor(), threadName);
thread.setDaemon(true);
thread.start();
}
ContainerBackgroundProcessor的run方法:
@Override
public void run() {
Throwable t = null;
String unexpectedDeathMessage = sm.getString(
"containerBase.backgroundProcess.unexpectedThreadDeath",
Thread.currentThread().getName());
try {
while (!threadDone) {
try {
Thread.sleep(backgroundProcessorDelay * 1000L);
} catch (InterruptedException e) {
// Ignore
}
if (!threadDone) {
// 获取当前的容器
Container parent = (Container) getMappingObject();
ClassLoader cl =
Thread.currentThread().getContextClassLoader();
// System.out.println("ContainerBackgroundProcessor在运行"+ parent.getName());
if (parent.getLoader() != null) {
System.out.println(parent.getName() + "有loader");
cl = parent.getLoader().getClassLoader();
}
// 执行子容器的background
processChildren(parent, cl);
}
}
} catch (RuntimeException e) {
t = e;
throw e;
} catch (Error e) {
t = e;
throw e;
} finally {
if (!threadDone) {
log.error(unexpectedDeathMessage, t);
}
}
}
最终调用到
ContainerBase的backgroundProcess()方法
public void backgroundProcess() {
if (!getState().isAvailable())
return;
Cluster cluster = getClusterInternal();
if (cluster != null) {
try {
cluster.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e);
}
}
// 热加载
Loader loader = getLoaderInternal(); // Context.webapploader
if (loader != null) {
try {
loader.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.loader", loader), e);
}
}
// 处理session
Manager manager = getManagerInternal();
if (manager != null) {
try {
manager.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.manager", manager), e);
}
}
Realm realm = getRealmInternal();
if (realm != null) {
try {
realm.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.realm", realm), e);
}
}
Valve current = pipeline.getFirst();
while (current != null) {
try {
current.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.valve", current), e);
}
current = current.getNext();
}
fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
}
其中loader.backgroundProcess();调用
到WebappLoader的backgroundProcess方法
@Override
public void backgroundProcess() {
if (reloadable && modified()) {
System.out.println(container.getInfo()+"触发了热加载");
try {
Thread.currentThread().setContextClassLoader
(WebappLoader.class.getClassLoader());
if (container instanceof StandardContext) {
((StandardContext) container).reload();
}
} finally {
if (container.getLoader() != null) {
Thread.currentThread().setContextClassLoader
(container.getLoader().getClassLoader());
}
}
} else {
closeJARs(false);
}
}
然后判断 如果热加载 调用StandardContext的reload方法
@Override
public synchronized void reload() {
// Validate our current component state
if (!getState().isAvailable())
throw new IllegalStateException
(sm.getString("standardContext.notStarted", getName()));
if(log.isInfoEnabled())
log.info(sm.getString("standardContext.reloadingStarted",
getName()));
// Stop accepting requests temporarily.
setPaused(true);//
try {
stop();
} catch (LifecycleException e) {
log.error(
sm.getString("standardContext.stoppingContext", getName()), e);
}
try {
start();
} catch (LifecycleException e) {
log.error(
sm.getString("standardContext.startingContext", getName()), e);
}
setPaused(false);
if(log.isInfoEnabled())
log.info(sm.getString("standardContext.reloadingCompleted",
getName()));
}
热加载 说白了就是 stop直接干掉原来的类加载器 设置为null 然后启动点的时候 new一个新的
为什么只有开发用用热加载 因为对象会被回收 但是 class对象很难被回收 所以生产 频繁热加载 那样 jvm内存占用会是问题 还有异步线程的回收问题 会发生内存泄漏
我们可以在Context上配置reloadable属性为true,这样就表示该应用开启了热加载功能,默认是false。
热加载触发的条件是:WEB-INF/classes目录下的文件发生了变化,WEB-INF/lib目录下的jar包添加、删除、修改都会触发热加载。
热加载大致流程为:
我们着重来分析一下第2、3步。
tomcat paused为true 就会停机 暂停 不接收请求 (热部署和热加载的时候paused就会为true)
我们不妨先来分析第3步-启动当前Context的过程中会发生什么事情:
加载类过程:
这是第3步,我们在来看第2步:
对于第2步-停止当前Context,其实所做的事情比较单一,就是清空和销毁,而其中跟类加载相关就是清空上文中的缓存对象。
这样,我们的热加载就是先清空所有东西,然后重新启动我们应用,但是因为这个的触发条件基本上是class类发生了变化,所以热加载的过程中关于应用其他的一些属性是没有发生变化的,比如你现在想在Context中添加一个Vavle是不会触发热加载的,而如果要达到这个效果就要用到热部署。
注意:虽然我们在热加载的过程发现它是先停止再启动,做法看似粗暴,但是这样是性价比比较高的,并且这种方式至少比重启Tomcat效率要高很多。
注意:热加载不能用于war包
关于类的加载,这里有一点是需要注意的,对于一个class文件所表示的类,同一个类加载器的不同实例,都可以加载这个类,并且得到的class对象是不同的,回到热加载,我们举一个例子,我们现在有一个A类,一个自定义的WebappClassloader类,一开始先用一个WebappClassloader实例加载A类,那么在jvm中就会存在一个A类的class对象,然后进行热加载,先停止,再启动,在停止的时候会杀掉当前应用的所有线程(除开真正执行代码的线程),再启动时又会生成一个WebappClassloader实例来加载A类,如果热加载之前的那个A类的class对象还没有被回收的话,那么此时jvm中其实会存在两个A类的class对象,这是不冲突,因为class对象的唯一标志是类加载器实例对象+类的全限定名。
tomcat启动的时候会有启动一个线程每隔一段时间会去判断应用中加载的类是否发生变法(类总数的变化,类的修改),如果发生了变化就会把应用的启动的线程停止掉,清除引用,并且把加载该应用的WebappClassLoader设为null,然后创建一个新的WebappClassLoader来重新加载应用。 tomcat中热部署发现类变法之后要做的一系列停止工作的时序图如下:
还进行了清除应用线程等工作。最后在WebappClassLoader的stopInternal()方法中执行了 classLoader = null; 那这个类加载器的实例就没有被引用了。 最后调用WebappLoader中的startInternal()方法,创建新的WebappClassLoader实例,然后开始重新加载应用。到此tomcat的热部署流程就完成了。
热部署跟热加载的本质区别是,热部署会重新部署 Web 应用,原来的 Context 对象会整个被销毁掉,因此这个 Context 所关联的一切资源都会被销毁,包括 Session。
那么 Tomcat 热部署又是由哪个容器来实现的呢?应该不是由 Context,因为热部署过程中 Context 容器被销毁了,那么这个重担就落在 Host 身上了,因为它是 Context 的父容器。
BackgroundProcessor线程第六步会发出一个PERIODIC_EVENT事件,而HostConfig监听了此事件,当接收到此事件后就会执行热部署的检查与操作。
对于一个文件夹部署的应用,通常会检查以下资源是否发生变动:
对于一个War部署的应用,会检查以下资源是否发生变动:
对于一个描述符部署的应用,会检查以下资源是否发生变动:
一旦这些文件或目录发生了变化,就会触发热部署,当然热部署也是有开关的,在Host上,默认是开启的。这里需要注意的是,对于一个目录是否发生了变化,Tomcat只判断了这个目录的修改时间是否发生了变化,所以和热加载是不冲突的,因为热加载监听的是WEB-INF/classes和WEB-INF/lib目录,而热部署监听的是应用名那一层的目录。
在讲热部署的过程之前,我们要先讲一下应用部署的优先级,对于一个应用,我们可以在四个地方进行定义:
优先级就是上面所列的顺序,意思是同一个应用名,如果你在这个四个地方都配置了,那么优先级低的将不起作用。因为Tomcat在部署一个应用的时候,会先查一下这个应用名是否已经被部署过了。
热部署的过程:
如果发生改变的是文件夹,比如/tomcat-7/webapps/应用名,那么不会做什么事情,只是会更新一下记录的修改时间,这是因为这个/tomcat-7/webapps/应用名目录下的文件,要么是jsp文件,要么是其他文件,而Tomcat只会管jsp文件,而对于jsp文件如果发生了修改,jsp自带的机制会处理修改的。
如果发生改变的是/tomcat-7/conf/Catalina/localhost/应用名.xml文件,那么就是先undeploy,然后再deploy,和热加载其实类似。对于undeploy就不多说了,就是讲当前应用从host从移除,这就包括了当前应用的停止和销毁,然后还会从已部署列表中移除当前应用,然后调用deployApps()就可以重新部署应用了。