关于tomcat和classloader的文章,网上多如牛毛,且互相转载,所以大多数搜到的基本上是讲到了tomcat中classloader的几个层次,对于初接触classloader,看了之后还是只知其然不知其所以然。

一直比较好奇,为什么tomcat需要实现自己的classloader,jvm提供的classloader有什么不符合需要?

事实上,tomcat之所以造了一堆自己的classloader,大致是出于下面三类目的:

  • 对于各个webapp中的class和lib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况;而对于许多应用,需要有共享的lib以便不浪费资源,举个例子,如果webapp1和webapp2都用到了log4j,可以将log4j提到tomcat/lib中,表示所有应用共享此类库,试想如果log4j很大,并且20个应用都分别加载,那实在是没有必要的。
  • 第二个原因则是与jvm一样的安全性问题。使用单独的classloader去装载tomcat自身的类库,以免其他恶意或无意的破坏;
  • 第三个原因是热部署的问题。相信大家一定为tomcat修改文件不用重启就自动重新装载类库而惊叹吧。

本文集中探讨第一个和第三个原因,即tomcat中如何利用classloader做到部分隔离,部分共享的,以及tomcat如何做到热部署的。

首先,我们讨论tomcat中如何做到lib的部分隔离,部分共享的。在Bootstrap中,可以找到如下代码:

private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}

应该可以看出来,这里创建了3个classloader,分别是common,server和shared,并且common是server和shared之父。如果感兴趣,可以看下createClassLoader,它会调用进ClassLoaderFactory.createClassLoader,这个工厂方法最后会创建一个StandardClassLoader,StandardClassLoader仅仅继承了URLClassLoader而没有其他更多改动,也就是说上面3个classloader都是StandardClassLoader,除了层次关系之外,他们与jvm定义的classloader并没有区别,这就意味着他们同样遵循双亲委派模型,只要我们能够用它装载指定的类,则它就自然的嵌入到了jvm的classloader体系中去了。Tomcat的classloader体系如图:

问题来了,tomcat是如何将自己和webapp的所有类用自己的classloader加载的呢?是否需要有个专门的地方遍历所有的类并将其加载,可是代码里并不能找到这样的地方。而且相对来说,将不用的类显式的加载进来也是一种浪费,那么,tomcat(或者说jvm)是如何做到这点呢?

这里有个隐式加载的问题,所谓的隐式加载,就是指在当前类中所有new的对象,如果没有被加载,则使用当前类的类加载器加载,即this.getClass(),getClassLoader()会默认加载该类中所有被new出来的对象的类(前提是他们没在别处先被加载过)。从这里思考,我们一个一个的应用,本质上是什么样子,事实上,正如所有程序都有一个main函数一样,所有的应用都有一个或多个startup的类(即入口),这个类是被最先加载的,并且随后的所有类都像树枝一样以此类为根被加载,只要控制了加载该入口的classloader,等于就控制了所有其他相关类的classloader。

以此为线索来看tomcat的Bootstrap中的init代码:

public void init()
throws Exception
{ // Set Catalina path
setCatalinaHome();
setCatalinaBase(); initClassLoaders(); Thread.currentThread().setContextClassLoader(catalinaLoader); SecurityClassLoad.securityClassLoad(catalinaLoader); // Load our startup class and call its process() method
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
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; }

在catalinaLoader.loadClass之后,Catalina事实上就由server这个classloader加载进来了,而下一句newInstance时,所有以Catalina为根的对象的类也会全部被隐式加载进来,但是为什么这里需要在其后费尽笔墨反射去setParentClassLoader呢,直接用((Catalina)startupInstance).setParentClassLoader岂不是更加方便?要注意,如果这样写,这个强制转换的Catalina便会由加载BootStrap的classloader(URLClassLoader)加载进来,而startupInstance是由StandardClassLoader加载进来的,并不是一个class,由此会抛一个ClassCastException。这也是类库可能发生冲突的一个原因。

有同学问到为什么在eclipse中调试tomcat源码时把反射换成((Catalina)startupInstance).setParentClassLoader是完全合法的,没有报任何异常。这里需要注意tomcat的启动默认会把bin下的bootstrap.jar加入classpath:set "CLASSPATH=%CLASSPATH%%CATALINA_BASE%\bin\tomcat-juli.jar;%CATALINA_HOME%\bin\bootstrap.jar",而eclipse中调试tomcat是所有相关类都在classpath的,区别在于,第一种情况,双亲委派模型在上层找不到Catalina.class,则StandardClassLoader去lib下加载catalina.jar;而第二种情况,AppClassLoader直接能够找到Catalina.class,所以就由他加载了,StandardClassLoader就形同虚设了。所以我们不能单从现象去判断原因,这也是我们为什么要学习classloader加载原理的原因。

搞明白这点,其实就可以理解tomcat是如何使用自己的classloader加载类进来并且如何隔离server和shared类的加载了。

但是另一个问题,tomcat又是如何隔离不同的webapp的加载呢?

对于每个webapp应用,都会对应唯一的StandContext,在StandContext中会引用WebappLoader,该类又会引用WebappClassLoader,WebappClassLoader就是真正加载webapp的classloader。

StandContext隶属于Lifecycle管理,在start方法中会做一系列准备工作(有兴趣可以参考,实际上该方法比较重要,但是篇幅太长),比如新建WebappClassLoader,另外loadOnStartup便会加载所有配置好的servlet(每个StandardWrapper负责管理一个servlet),这里同样的一个问题是,在我们自己写的web应用程序中,入口是什么?答案就是Servlet, Listener, Filter这些组件,如果我们控制好入口的classloader,便等于控制了其后所加载的全部类,那么,tomcat是如何控制的呢?且看StandardWrapper中一个重要的方法loadServlet(篇幅所限,隐去了大部分不想关内容),getLoader()事实上调用到了StandContext中保存的WebappLoader,于是,用该loader加载Servlet,从而控制住了Servlet中所有待加载的类。

public synchronized Servlet loadServlet() throws ServletException {

        ...

        Servlet servlet;
try {
... // Acquire an instance of the class loader to be used
Loader loader = getLoader();
if (loader == null) {
unavailable(null);
throw new ServletException
(sm.getString("standardWrapper.missingLoader", getName()));
} ClassLoader classLoader = loader.getClassLoader(); // Special case class loader for a container provided servlet
//
if (isContainerProvidedServlet(actualClass) &&
! ((Context)getParent()).getPrivileged() ) {
// If it is a priviledged context - using its own
// class loader will work, since it's a child of the container
// loader
classLoader = this.getClass().getClassLoader();
} // Load the specified servlet class from the appropriate class loader
Class classClass = null;
try {
if (SecurityUtil.isPackageProtectionEnabled()){
...
} else {
if (classLoader != null) {
classClass = classLoader.loadClass(actualClass);
} else {
classClass = Class.forName(actualClass);
}
}
} catch (ClassNotFoundException e) {
unavailable(null);
getServletContext().log( "Error loading " + classLoader + " " + actualClass, e );
throw new ServletException
(sm.getString("standardWrapper.missingClass", actualClass),
e);
} if (classClass == null) {
unavailable(null);
throw new ServletException
(sm.getString("standardWrapper.missingClass", actualClass));
} // Instantiate and initialize an instance of the servlet class itself
try {
servlet = (Servlet) classClass.newInstance();
// Annotation processing
if (!((Context) getParent()).getIgnoreAnnotations()) {
if (getParent() instanceof StandardContext) {
((StandardContext)getParent()).getAnnotationProcessor().processAnnotations(servlet);
((StandardContext)getParent()).getAnnotationProcessor().postConstruct(servlet);
}
}
} catch (ClassCastException e) {
...
} catch (Throwable e) {
...
} ...
return servlet; }

这里的加载过程与之前的一致,至于如何做到不同webapp之间的隔离,我想大家已经明白,不同的StandardContext有不同的WebappClassLoader,那么不同的webapp的类装载器就是不一致的。装载器的不一致带来了名称空间不一致,所以webapp之间是相互隔离的。

关于tomcat是如何做到热部署的,相信不用说也能猜到个十之八九。简单讲就是定期检查是否需要热部署,如果需要,则将类装载器也重新装载,并且去重新装载其他相关类。关于tomcat是如何做的,可以具体看以下分析。

首先来看一个后台的定期检查,该定期检查是StandardContext的一个后台线程,会做reload的check,过期session清理等等,这里的modified实际上调用了WebappClassLoader中的方法以判断这个class是不是已经修改。注意到他调用了StandardContext的reload方法。

public void backgroundProcess() {
if (reloadable && modified()) {
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);
}
}

那么reload方法具体做了什么?非常简单,就是tomcat lifecycle中标准的启停方法stop和start,别忘了,start方法会重新造一个WebappClassLoader并且重复loadOnStartup的过程,从而重新加载了webapp中的类,注意到一般应用很大时,热部署通常会报outofmemory: permgen space not enough之类的,这是由于之前加载进来的class还没有清除而方法区内存又不够的原因:

public synchronized void reload() {

        // Validate our current component state
if (!started)
throw new IllegalStateException
(sm.getString("containerBase.notStarted", logName())); // Make sure reloading is enabled
// if (!reloadable)
// throw new IllegalStateException
// (sm.getString("standardContext.notReloadable"));
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); }

原文参考:http://blog.csdn.net/liweisnake/article/details/8470285

浅议tomcat与classloader的更多相关文章

  1. ClassLoader,Thread.currentThread().setContextClassLoader,tomcat的ClassLoader

    实际上,在Java应用中所有程序都运行在线程里,如果在程序中没有手工设置过ClassLoader,对于一般的java类如下两种方法获得的ClassLoader通常都是同一个 this.getClass ...

  2. 网络语音视频技术浅议(附多个demo源码下载)

    我们在开发实践中常常会涉及到网络语音视频技术.诸如即时通讯.视频会议.远程医疗.远程教育.网络监控等等,这些网络多媒体应用系统都离不开网络语音视频技术.本人才疏学浅,对于网络语音视频技术也仅仅是略知皮 ...

  3. 浅谈tomcat中间件的优化【转】

    今天来总结一下tomcat的一些优化的方案,由于本人才疏学浅,写的不好,勿喷! tomcat对于大多数从事开发工作的童鞋应该不会很陌生,通常做为默认的开发环境来为大家服务,不过tomcat默认的一些配 ...

  4. 关于tomcat的classloader的一点想法

    关于tomcat的classloader相关的帖子网上非常多,我觉得比较好的有: https://www.jianshu.com/p/d90e4430b0b9 https://blog.csdn.ne ...

  5. tomcat的classloader机制

    本系列博客打算分析一下tomcat7.x的源码,其中可能会穿插一些java基础知识的介绍  读tomcat的源码的时候,我建议和官方的User Guide一起阅读,明白tomcat做某件事情的目的之后 ...

  6. 浅议Delphi中的Windows API调用(举的两个例子分别是String和API,都不错,挺具有代表性)

    浅议Delphi中的Windows API调用http://tech.163.com/school • 2005-08-15 10:57:41 • 来源: 天极网为了能在Windows下快速开发应用程 ...

  7. 浅议Grpc传输机制和WCF中的回调机制的代码迁移

    浅议Grpc传输机制和WCF中的回调机制的代码迁移 一.引子 如您所知,gRPC是目前比较常见的rpc框架,可以方便的作为服务与服务之间的通信基础设施,为构建微服务体系提供非常强有力的支持. 而基于. ...

  8. 浅读tomcat架构设计之tomcat生命周期(2)

    浅读tomcat架构设计和tomcat启动过程(1) https://www.cnblogs.com/piaomiaohongchen/p/14977272.html tomcat通过org.apac ...

  9. 浅读tomcat架构设计之tomcat容器Container(3)

    浅读tomcat架构设计和tomcat启动过程(1) https://www.cnblogs.com/piaomiaohongchen/p/14977272.html 浅读tomcat架构设计之tom ...

随机推荐

  1. 同时使用Binding&StringFormat 显示Text【项目】

    Case ID (?unit) 红色的字根据一个后台boolean来做trigger,可以是Case or Open 蓝色的字binding到后台的一个string属性来切换任意的Unit单位 这样一 ...

  2. SQL SERVER 数据库表同步复制 笔记

    SQL SERVER 数据库表同步复制 笔记 同步复制可运行在不同版本的SQL Server服务之间 环境模拟需要两台数据库192.168.1.1(发布),192.168.1.10(订阅) 1.在发布 ...

  3. Android中Webview使用javascript调用事先定义好的Java函数

    1. 首先定义好一个类,专们用于给javascript调用 public class JavaScriptInterface { // share your news public void shar ...

  4. Codeforces Round #308 (Div. 2) A. Vanya and Table 暴力

    A. Vanya and Table Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://codeforces.com/contest/552/pr ...

  5. 自定义WM_NOTIFY消息

    自定义WM_NOTIFY消息 习惯了用自定义用户消息进行各种状态的通知,特别是子窗口与父窗口之间的交互.但ON_MESSAGE没有控件ID的限制,如果有多个子窗口发送同一个消息给父窗口时,父窗口就不知 ...

  6. 搭建Spring + SpringMVC + Mybatis框架之二(整合Spring和Mybatis)

    整合Spring和Mybatis 首先给出完整的项目目录: (1)引入项目需要的jar包 使用http://maven.apache.org作为中央仓库即可. Spring核心包,mybatis核心包 ...

  7. CloudStack的VO在调用setRemoved方法抛异常的原因

    今天在开发中发现一个问题,本来想对一个VO对象的removed值赋值,然后去update一下这条记录,一个最简单的set方法,但是在调用时直接抛异常了. 1: public void setRemov ...

  8. [C++基础]C++中静态成员函数如何访问非静态成员

    #include <iostream> /* 静态成员函数只能访问静态数据成员.静态成员函数和类以外的函数和数据,不能访问非静态数据成员,但静态成员函数或静态数据成员可由任意访问许可的函数 ...

  9. android之BitmapFactory.Options的使用

    怎样获取图片的大小? 首先我们把这个图片转成Bitmap,然后再利用Bitmap的getWidth()和getHeight()方法就可以取到图片的宽高了. 新问题又来了,在通过BitmapFactor ...

  10. linux下的十六进制编辑器---wxHexEdit

    ....其实wxHexEdit是一个跨平台的十六进制编辑器,支持windows,linux,mac. 之所以标题用linux...是因为windows下多数都用winhex,UE之类的编辑器,而lin ...