浅议tomcat与classloader
关于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的更多相关文章
- ClassLoader,Thread.currentThread().setContextClassLoader,tomcat的ClassLoader
实际上,在Java应用中所有程序都运行在线程里,如果在程序中没有手工设置过ClassLoader,对于一般的java类如下两种方法获得的ClassLoader通常都是同一个 this.getClass ...
- 网络语音视频技术浅议(附多个demo源码下载)
我们在开发实践中常常会涉及到网络语音视频技术.诸如即时通讯.视频会议.远程医疗.远程教育.网络监控等等,这些网络多媒体应用系统都离不开网络语音视频技术.本人才疏学浅,对于网络语音视频技术也仅仅是略知皮 ...
- 浅谈tomcat中间件的优化【转】
今天来总结一下tomcat的一些优化的方案,由于本人才疏学浅,写的不好,勿喷! tomcat对于大多数从事开发工作的童鞋应该不会很陌生,通常做为默认的开发环境来为大家服务,不过tomcat默认的一些配 ...
- 关于tomcat的classloader的一点想法
关于tomcat的classloader相关的帖子网上非常多,我觉得比较好的有: https://www.jianshu.com/p/d90e4430b0b9 https://blog.csdn.ne ...
- tomcat的classloader机制
本系列博客打算分析一下tomcat7.x的源码,其中可能会穿插一些java基础知识的介绍 读tomcat的源码的时候,我建议和官方的User Guide一起阅读,明白tomcat做某件事情的目的之后 ...
- 浅议Delphi中的Windows API调用(举的两个例子分别是String和API,都不错,挺具有代表性)
浅议Delphi中的Windows API调用http://tech.163.com/school • 2005-08-15 10:57:41 • 来源: 天极网为了能在Windows下快速开发应用程 ...
- 浅议Grpc传输机制和WCF中的回调机制的代码迁移
浅议Grpc传输机制和WCF中的回调机制的代码迁移 一.引子 如您所知,gRPC是目前比较常见的rpc框架,可以方便的作为服务与服务之间的通信基础设施,为构建微服务体系提供非常强有力的支持. 而基于. ...
- 浅读tomcat架构设计之tomcat生命周期(2)
浅读tomcat架构设计和tomcat启动过程(1) https://www.cnblogs.com/piaomiaohongchen/p/14977272.html tomcat通过org.apac ...
- 浅读tomcat架构设计之tomcat容器Container(3)
浅读tomcat架构设计和tomcat启动过程(1) https://www.cnblogs.com/piaomiaohongchen/p/14977272.html 浅读tomcat架构设计之tom ...
随机推荐
- Arrays, Hashtables and Dictionaries
Original article Built-in arrays Javascript Arrays(Javascript only) ArrayLists Hashtables Generic Li ...
- 免费的天气Web Service接口
免费的天气Web Service接口 在android应用当中很多时候需要获取天气的信息,这里提供怎么获取天气信息: 1. http://www.ayandy.com/Service.asmx?wsd ...
- 为什么要尽量少使用iframe
Iframes 阻塞页面加载 及时触发 window 的 onload 事件是非常重要的.onload 事件触发使浏览器的 “忙” 指示器停止,告诉用户当前网页已经加载完毕.当 onload 事件加载 ...
- QML学习笔记之一
摘自<Qt Quick中文手册> Qt Quick提供了一套高动态,丰富的QML元素来定制用户界面的说明性框架. Qt Quick包含了QtDeclarative C++模块.QML,并且 ...
- hdu 4499 Cannon dfs
Cannon Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://acm.hdu.edu.cn/showproblem.php?pid=4499 D ...
- This implementation is not part of the Windows Platform FIPS validated cryptographic algorithms. 此实现不是 Windows 平台 FIPS 验证的加密算法的一部分 解决方案
但web启用了md5加密后 有可能出现这样的错误 This implementation is not part of the Windows Platform FIPS validated cryp ...
- 【JavaScript】页面加载性能优化
核心在于:减少加载时间 1.减少请求次数 2.缩减文件大小 3.异步加载---------------------->比如document.write 4.延迟加载.动态加载---------- ...
- wordpress文章ID不连续显示问题的完美解决
在最新版的 wordpress 系统中,依然存在着文章ID不连续显示的问题,也就是我们还没有上传多少文章,在数据库里的ID号已经很大了,也就是说如果我们的博客使用的是固定链接,那么在前台显示的ID相差 ...
- The Kernel Newbie Corner: Kernel Debugging Using proc "Sequence" Files--Part 1
转载:https://www.linux.com/learn/linux-career-center/37985-the-kernel-newbie-corner-kernel-debugging-u ...
- GNU的ar,ranlib和nm
转:http://blog.csdn.net/yuntongsf/article/details/6284517 RANLIB 的作用: CC = CC=/usr/local/ndk/toolchai ...