类加载器内存泄露与tomcat自定义加载器
2015-12-14 10:59
441 查看
1 类加载器内存泄露
每个对象都持有对它的Class引用,也就持有对它的类加载器的引用。相反的,每个类加载器也持有对它加载过的类的引用,保存在Class的静态字段中。如下图所示(图片源自其他博客)这就意味着如果一个类的加载器发生了内存泄露,那么与之关联的类和所有的静态字段都发生了泄漏。这意味着一些缓存状态、单例以及配置和状态信息也就暴露了出去。即使你的程序没有保存这些信息,但并不意味着你使用的框架没有,因为他们的jar包也在server 的classpath中,由此来看这是非常危险的。
2 Tomcat的类加载模型
对于运行在 Java EE容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
2.1 目录合并
以Tomcat8为例,其类加载器的结构图如下所示,这与网上大多数帖子的图稍有差异,其实那是Tomcat5的结构图,在那个图示中,Tomcat定义了CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader,他们分别负责加载Tomcat目录下的/common/*、/server/*、/shared/*和/WebApp/WEB-INF/*中的Java类库。而在Tomcat6以后/common/*、/server/*、/shared/*三个文件夹合并为/lib目录。2.2 Tomcat类加载器
Bootstrap :这个Bootstrap是一类加载器的统称,由JVM提供的用来加载Java的jar包System:简单的说,这个加载器的主要作用是用于加载$CATALINA_HOME/bin/目录下的三个jar包:bootstrap.jar、tomcat-juli.jar和commons-daemon.jar。由它的位置也可以知道,通过这个classloader加载的所有类,都对tomcat自身的类,以及所有web应用的类可见
Common:用来加载tomcat自身的类的加载器。默认加载$CATALINA_HOME/lib/目录下的jar包或为打包的.class文件,而应用程序的依赖jar则不应该放在这个路径下。它的路径定义在$CATALINA_HOME/conf/catalina.properties文件中common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
WebappX :应用程序专属类加载器。Tomcat为每一个部署的Web应用创建一个类加载器,它用于加载/WEB-INF/lib和/WEB-INF/classes内的类文件,它仅对于自己的应用是可见的,而对其他部署在这个Tomcat实例上的Web应用是隔离的。
2.3 tomcat为什么要自定义类加载器
对于Tomcat来说,我认为最重要的就是为每个应用都分配了一个专属类加载器,主要有以下原因:1 为了不同webapp加载不同版本的jar包:在现在的web应用中,第三方框架的使用随处可见,但是如果两个webapp都使用了同一个jar包但是版本不同,那么就非常有必要进行隔离。我们知道一个classloader实例对同一个class文件仅能加载一次,在加载某class文件一个版本之后,如果再次搜索到同名的class文件是会抛出异常的。这样对于不同版本的jar包加载还是隔离开比较好。
2 当然是为了安全:由本人第一段可以看到,类加载器的内存泄露是致命的。而如果隔离开,则可以保证其他webapp是安全的
3 类的热部署:类加载器之所以能发展到今天,它能实现类的热部署才是王道。对于一个应用来说,如果在不用停机的情况下就可以进行升级显然是最完美的方式。在我们部署一个应用的时候,通常都会设置reload=true。它的实现就是靠类加载器。
3 Tomcat类加载器启动过程
下面这张图基本描述了Tomcat的启动过程3.1 bootstrap入口
tomcat的启动入口在org.apache.catalina.startup.Bootstrap类中,其main方法如下(仅列出与类加载器相关的逻辑):(1)Main方法 它是整个Tomcat启动的入口
<div style="text-align: justify;"><span style="font-family: 宋体;"> Bootstrap bootstrap = new Bootstrap();</span></div> try { <div style="text-align: justify;"><span style="font-family: 宋体;"> } catch (Throwable t) {</span></div> bootstrap.init();//初始化入口 <div style="text-align: justify;"><span style="font-family: 宋体;"> t.printStackTrace();</span></div> handleThrowable(t); return; } <div style="text-align: justify;"><span style="font-family: 宋体;"> daemon = bootstrap;</span></div>
(2)init方法
在init方法里面,对各种类加载器进行了初始化,然后利用catalinaLoader加载了Catalina类并初始化Catalina对象,将Catalina的父加载器设置为sharedLoader。
/** *初始化各种类加载器 利用catalina类加载器加载Catalina对象 */ public void init() throws Exception { initClassLoaders();//初始化各种类加载器 Thread.currentThread().setContextClassLoader(catalinaLoader);//设置当前线程类加载器为catalinaLoader SecurityClassLoad.securityClassLoad(catalinaLoader); // Load our startup class and call its process() method //这里利用catalinaClassLoader去加载Catalina类 并初始化Catalina对象 if (log.isDebugEnabled()) log.debug("Loading startup class"); Class<?> startupClass = catalinaLoader.loadClass // Set the shared extensions class loader ("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.newInstance(); if (log.isDebugEnabled()) paramTypes[0] = Class.forName("java.lang.ClassLoader"); log.debug("Setting startup class properties"); String methodName = "setParentClassLoader"; Class<?> paramTypes[] = new Class[1]; Object paramValues[] = new Object[1]; method.invoke(startupInstance, paramValues);//设置Catalina对象的parentClassLoader 为sharedLoader paramValues[0] = sharedLoader; Method method = startupInstance.getClass().getMethod(methodName, paramTypes); catalinaDaemon = startupInstance; }
3.2 initClassLoader
在上面的方法中initClassLoaders方法用于初始化各种类加载,现在来看看他初始化了哪些类加载器。/*** 初始化commonClassLoader serverClassLoader sharedClassLoader */ private void initClassLoaders() { try { commonLoader = createClassLoader("common", null);
<span style="white-space:pre"> </span> //初始化commonloader
if( commonLoader == null ) { // no config file, default to this loader - we might be in a 'single' env. catalinaLoader = createClassLoader("server", commonLoader);
//初始化catalinaLoader
commonLoader=this.getClass().getClassLoader(); } sharedLoader = createClassLoader("shared", commonLoader);//初始化sharedLoader //创建指定名称的类加载器 并指定其父加载器
} catch (Throwable t) { handleThrowable(t); log.error("Class loader creation threw exception", t); System.exit(1); } } /** */ //在目前的tomcat默认配置中 仅指定了common.loader="${catalina.base}/lib"
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { //从$CATALINA_HOME/conf/catalina.properties文件中读取对应的classloader的路径 //"${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar" <div style="text-align: justify;"><span style="font-family: Arial, sans-serif;"> value = replace(value);</span></div> String value = CatalinaProperties.getProperty(name + ".loader"); //如果这个值为null,则这个加载器与其父类加载器值相同 //也就是说serverLoader和sharedLoader在默认情况下都是commonLoader if ((value == null) || (value.equals(""))) return parent; List<Repository> repositories = new ArrayList<>(); <div style="text-align: justify;"><span style="font-family: Arial, sans-serif;"> new Repository(repository, RepositoryType.URL));</span></div> String[] repositoryPaths = getPaths(value); for (String repository : repositoryPaths) { // Check for a JAR URL repository try { @SuppressWarnings("unused") URL url = new URL(repository); repositories.add( continue; new Repository(repository, RepositoryType.GLOB));
} catch (MalformedURLException e) { // Ignore } // Local repository if (repository.endsWith("*.jar")) { repository = repository.substring (0, repository.length() - "*.jar".length()); repositories.add( return ClassLoaderFactory.createClassLoader(repositories, parent);
}
else if (repository.endsWith(".jar")) { repositories.add( new Repository(repository, RepositoryType.JAR)); } else { repositories.add( new Repository(repository, RepositoryType.DIR)); } }
看过以上两个方法的代码,可以看到这个过程实际上初始化了三个类加载器,commonLoader、sharedLoader和catalinaLoader。而在在目前的tomcat默认配置中仅指定了common.loader的值,根据这个值可以看出它主要就是载入Tomcat服务器根路径下lib文件夹里面的资源,也就是说在默认情况下,tomcat中的这三个类加载器都为commonLoader。
到目前为止,我们已经完成了tomcat初始化的前期工作,初始化了catalina对象然后设置了以下关系
(1)Thread.currentThread().setContextClassLoader 设置为catalinaLoader
(2) catalina.setParent() 设置为sharedLoader
3.3 start方法
完成了一些初始化工作之后,tomcat就要调用catalina的start方法来启动。这里仅贴出与类加载器相关的一些代码(1)在start方法里面,最关键的就是load方法,它主要是为了加载conf/server.xml文件,并生成server对象。然后调用server.start方法完成tomcat的启动
<div style="text-align: justify;"><span style="font-family: Arial, sans-serif;">if (getServer() == null) {</span></div><div style="text-align: justify;"><span style="font-family: Arial, sans-serif;"> load();//加载server.xml文件 生成server对象</span></div> } if (getServer() == null) { <div style="text-align: justify;"><span style="font-family: Arial, sans-serif;"> log.fatal("Cannot start server. Server instance is not configured.");</span></div> return; } long t1 = System.nanoTime(); // Start the new server try { <div style="text-align: justify;"><span style="font-family: Arial, sans-serif;"> }</span></div><div style="text-align: justify;"><span style="font-family: Arial, sans-serif;"> getServer().start();//启动最终的tomcat服务器 其实server要做的是启动里面的service </span></div>
(2)在load方法里 ,关键是创建了Digester对象,它负责用它来解析conf/server.xml文件,根据配置的信息来创建相应的server对象。
Digester digester = createStartDigester();
(3)在Digester对象创建过程中,涉及到了classloader相关的部分如下。这里设置了Digester对象在创建对象的时候使用当前线程contextClassLoader。再结合上面看到,这个classLoader实际就是catalinaLoader。也就是说Server对象都是由catalinaLoader创建的。
digester.setUseContextClassLoader(true); //将useContextClassLoader参数设置为true,那么待会将会用预先保存的线程classLoader来载入class,这里其实就是catalinaloader
(4)在Digester对象创建中还有关于Engine的一条规则,也就是将Engine的ClassLoader设置为parentClassLoader,而在前面看到 parentClassLoader是sharedLoader,也就是说Engine的parentClassLoader也会是sharedLoader。
(5) 在Digester对象创建中还有关于Host的一条规则,这里将Host的parentClassLoader设置为engine的parentClassLoader,那么Host的parentClassLoader 也同样是sharedLoader
<span style="white-space:pre"> </span>//创建host对象 digester.addObjectCreate(prefix + "Host", "org.apache.catalina.core.StandardHost", "className"); digester.addSetProperties(prefix + "Host");<span style="font-family: Arial, sans-serif;">//创建host对象的配置</span> digester.addRule(prefix + "Host",new CopyParentClassLoaderRule()); //会将host的parentClassloader设置为engine的,engine被设置为sharedloader
到现在为止,我们再对tomcat的各种对象的classloader进行一次梳理
Catalina 对象的parentClassLoader :sharedLoader
Engine 对象的parentClassLoader :sharedLoader
Host 对象的parentClassLoader :sharedLoader
3.4Context
在前面的Engine Host对象都创建完成之后,最重要就是要创建Context对象。Context对象的启动是在StandardContext类的startInternal方法中。这个方法中与类加载器相关的部分如下:if (getLoader() == null) { WebappLoader webappLoader = new WebappLoader(getParentClassLoader()); webappLoader.setDelegate(getDelegate()); setLoader(webappLoader); }
(1)从这可以看到初始化了一个webappLoader,并设置为当前的Context的classloader。而这里创建的webapploader指定了它的parentLoader为当前context的parentloader,我们知道context上一层就是Host,也就是说webapploader的parentClassloader是sharedLoader。
(2) 下面进入到webapploader的startInternal方法。在这里面有一行代码创建了classLoader,这里实际上创建的是一个WebappClassLoaderBase对象。
classLoader = createClassLoader();// //创建webappclassLoader,这里会将sharedLoader设置为parent private WebappClassLoader createClassLoader() throws Exception { Class<?> clazz = Class.forName(loaderClass); //获取要创建的classLoader的class引用 org.apache.catalina.loader.WebappClassLoader WebappClassLoader classLoader = null; if (parentClassLoader == null) { parentClassLoader = context.getParentClassLoader(); //获取context的parentClassLoader,这里是sharedLoader } Class<?>[] argTypes = { ClassLoader.class }; Object[] args = { parentClassLoader }; Constructor<?> constr = clazz.getConstructor(argTypes); //获取构造函数,这里需要传递一个parentClassLoader,其实这里的双亲loader就是sharedLoader classLoader = (WebappClassLoader) constr.newInstance(args); return classLoader; }
(3)在webapploader的startInternal方法中有一句调用WebappClassLoader的start方法,在这个方法里就有我们非常熟悉的/WEB-INF/classes和/WEB-INF/lib啦。
public void start() throws LifecycleException { WebResource classes = resources.getResource("/WEB-INF/classes"); //获取/WEB-INF/classes目录的资源引用 if (classes.isDirectory() && classes.canRead()) { addURL(classes.getURL()); //将该资源添加到当前classLoader的资源库 } WebResource[] jars = resources.listResources("/WEB-INF/lib"); //这里是获取lib文件夹 for (WebResource jar : jars) { //遍历所有的资源 if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) { addURL(jar.getURL()); // 将资源加入到classLoader的资源库 jarModificationTimes.put( jar.getName(), Long.valueOf(jar.getLastModified())); } } started = true; String encoding = null; try { encoding = System.getProperty("file.encoding"); } catch (SecurityException e) { return; } if (encoding.indexOf("EBCDIC")!=-1) { needConvert = true; } }
到这为止,就浏览了tomcat启动过程中与classLoader有关的代码。再回头去看看最开始那张结构图,实际上我们在讲common及以下的部分。在Tomcat6以后,尽管在目录下已经找不到shared、common、server这样的文件夹,但是在源码中,这样的结构并没有改变,只是默认情况下他们都是common。
3.5 StandardWrap
对于一个web应用来说,我们实现功能的地方无非是在Servlet、Listener、Filter中,那么这三个是如何构造出来的呢?他们实际对应的是StandardWrapper、ApplicationListener、FilterDef、FilterMap的实例。而他们的构造全部是在org.apache.catalina.core.StandardContext类的startInternal方法中,下面以Servlet为例,在startInternal方法中有一个loadOnStartup方法,它是负责加载所有的"loadon startup" Servlet,而真正实现servlet加载的是在StandardWrapper中的load方法:
InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager(); try { servlet = (Servlet) instanceManager.newInstance(servletClass); }
这里仅列出该方法关键的语句。可以看到实际上一个Servlet的初始化是由instaneManager实现的。而这个instaneManager是由context持有的,现在来猜想一下,instaneManager里一定持有了某个classLoader来加载Servlet类。
下面来认证我们的想法,每一个context中都持有一个DefaultInstanceManager实例,在这里面定义了newInstance方法:
public Object newInstance(String className) throws IllegalAccessException, InvocationTargetException, NamingException, InstantiationException, ClassNotFoundException { Class<?> clazz = loadClassMaybePrivileged(className, classLoader); return newInstance(clazz.newInstance(), clazz); }
这里面我们看到了classLoader,那么这个classLoader到底是谁呢?在DefaultInstanceManager的构造方法里面,看到了它的身影
classLoader = catalinaContext.getLoader().getClassLoader();
这面的catalinaContext的就是前面的3.4节提到的context,那么这个classLoader也就是webappClassLoader了
3.6 示例验证
这里来写一个简单的servlet ,然后获取它的全部classLoaderprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println(this.getClass().getClassLoader()); java.lang.ClassLoader classLoader=this.getClass().getClassLoader(); System.out.println("============="); while(classLoader!=null) { System.out.println("加载器:"+classLoader.getClass().getCanonicalName()); classLoader=classLoader.getParent(); } }
输出的结果为:
WebappClassLoader context: WelcomPage delegate: false ----------> Parent Classloader: java.net.URLClassLoader@2626b418 ============= 加载器:org.apache.catalina.loader.WebappClassLoader 加载器:java.net.URLClassLoader 加载器:sun.misc.Launcher.AppClassLoader 加载器:sun.misc.Launcher.ExtClassLoader
这里面显然WebappClassLoader重写了toString方法,所以我们利用getClass().getCanonicalName()方式类获取classLoader的类名。
第一个输出说明servlet的classLoader是WebappClassLoader,
第二个输出是一个URLClassLoader的对象,按照我们前面所知,它应该是sharedLoader才对啊?sharedLoader又是什么呢?默认情况下是commonLoader。难道commonLoader是URLClassLoader吗?没错就是这样的。在Bootstrap的createClassLoader方法中,利用ClassLoaderFactory类的createClassLoader方法来创建classLoader,而这个方法实际上就是实例化了一个URLClassLoader。
return AccessController.doPrivileged( new PrivilegedAction<URLClassLoader>() { @Override public URLClassLoader run() { if (parent == null) return new URLClassLoader(array); else return new URLClassLoader(array, parent); } });
第三行第四行的输出就不用解释了,他们显然是JVM的类加载器。没有他们哪有Java运行环境呢?
现在再看看上面的流程图,是不是就完全理解了?
4 Tomcat类的热部署
在用tomcat进行开发的时候,我们都有这样的经历,启动着tomcat的时候修改web工程内的源代码,很短时间内就会自动加载到web工程内,而并不需要重新启动tomcat。这就是热部署,那么Tomcat是如何实现的呢?在StandardContext里面运行着一个backgroundProcess,它实际去调用webappClassloader的backgroundProcess方法public void backgroundProcess() { if (reloadable && modified()) {//检测是否有改变并且当前应用是否允许重新加载 try { Thread.currentThread().setContextClassLoader (WebappLoader.class.getClassLoader()); if (context != null) { context.reload();//重新加载 } } finally { if (context != null && context.getLoader() != null) { Thread.currentThread().setContextClassLoader (context.getLoader().getClassLoader()); } } } }
这里面的reload方法实际上就是就是重复前面3.4里面的startInternal过程。
而前面提到的modified方法就是负责去检查"/WEB-INF/lib"和/WEB-INF/classes是否有修改
/** * 检查/WBE-INF/lib和/WEB-INF/classes路径是否发生改变 */ public boolean modified() { if (log.isDebugEnabled()) log.debug("modified()"); for (Entry<String,ResourceEntry> entry : resourceEntries.entrySet()) { long cachedLastModified = entry.getValue().lastModified;//获取文件的最后一次修改时间 long lastModified = resources.getClassLoaderResource( entry.getKey()).getLastModified(); if (lastModified != cachedLastModified) {//发生了修改 if( log.isDebugEnabled() ) log.debug(sm.getString("webappClassLoader.resourceModified", entry.getKey(), new Date(cachedLastModified), new Date(lastModified))); return true; } } // Check if JARs have been added or removed WebResource[] jars = resources.listResources("/WEB-INF/lib");//检查jar包 // Filter out non-JAR resources int jarCount = 0; for (WebResource jar : jars) { if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) { jarCount++; Long recordedLastModified = jarModificationTimes.get(jar.getName()); if (recordedLastModified == null) {//如果在缓存中没有找到个jar的相关信息 证明是新加的jar包 // Jar has been added log.info(sm.getString("webappClassLoader.jarsAdded", resources.getContext().getName())); return true; } if (recordedLastModified.longValue() != jar.getLastModified()) {//最后一次修改时间发生了改变 // Jar has been changed log.info(sm.getString("webappClassLoader.jarsModified", resources.getContext().getName())); return true; } } } if (jarCount < jarModificationTimes.size()){ log.info(sm.getString("webappClassLoader.jarsRemoved", resources.getContext().getName())); return true; } // No classes have been modified return false; }
相关文章推荐
- java-模拟tomcat服务器
- i-jetty环境搭配与编译
- 实现单Tomcat多Server配置
- 生产环境下的Tomcat配置
- Linux部署Tomcat服务器
- jenkins------结合maven将svn项目自动部署到tomcat下
- 如何搞定tomcat这只喵~
- c语言内存泄露示例解析
- IE下使用jQuery重置iframe地址时内存泄露问题解决办法
- tomcat在opensuse下开机自启失败的原因分析及解决方法
- jsp项目中更改tomcat的默认index.jsp访问路径的方法
- Tomcat 多端口 多应用
- tomcat 5.0 + apache 2.0 完全安装步骤详解
- Tomcat安全设置 win2003 下tomcat权限限制
- Jsp和PHP共用80端口整合Apache和Tomcat(访问时无需加端口号)
- Tomcat服务器 安全设置第1/3页
- tomcat 6.0.20在一个机器上安装多个服务的方法
- Tomcat 5.5 数据库连接池配置
- Tomcat内存溢出分析及解决方法