Class加载器-内存泄漏问题的探讨2

本文介绍了解决Java应用中常见的ClassLoader类加载内存泄漏问题的方法。详细分析了多种导致内存泄漏的原因,包括第三方库的不当使用及特定组件的设计缺陷,并提出了针对性的解决方案。

严重警告: 如果存在任何来自应用以外的引用,引用了由J2EE应用本身类加载器加载的类和类实例对象,那么ClassLoader类加载内存泄漏问题就出现了。

ClassLoader 类加载内存泄漏问题的定位

正如上所述Classloader类加载内存泄漏是由于不断重复地启动、停止应用导致Class多次加载,并由于系统一级对象引用的关系,导致以前创建的类无法被回收,从而导致内存泄漏。

定位ClassLoader 类加载内存泄漏的根源还是非常困难的,因为任何现有的JVM Profiling工具都不能通过ClassLoader的视角来分析当前内存中所存在的Class类。 目前我们只能通过产生JVM Heapdump的方式来鉴定是否存在ClassLoader 类加载内存泄漏,再通过以上ClassLoader 类加载内存泄漏产生的机理来排查可能出现问题的地方,最终解决问题。

好像在JDK6工具集中提供了相应的工具来定位问题。请参考http://blogs.sun.com/fkieviet/entry/how_to_fix_the_dreaded

为了简化大家排查和定位应用中可能存在ClassLoader 类加载内存泄漏的过程,为此我们罗列了一些能导致Classloader类加载内存泄漏的代码和组件:(我们此处就不铺开篇幅阐述下面组件导致内存泄漏的根源,就当作大家的作业吧!哈哈)

a) 应用或使用的组件中使用了java.util.logging.Level那你得注意了。

b) 如果使用了诸如DBCP等基于DriverManager API基础上开发的数据库连接池组件,如果底层设计考虑不周,极易引发Classloader类加载内存泄漏。

c) 如果你使用到了commons-logging组件(其实很多OpenSource组件都依赖于commons-logging),那十有八九 会出现Classloader类加载内存泄漏。因为在象WebSphere、Tomcat等服务器核心引擎中同样使用到了commons-logging 组件,并在应用启动之前commons-logging的很多类已经被系统级ClassLoader所加载。缺省状态下,一个类的加载是从JVM类加载器 开始的,这样系统commons-logging的优先级一般高于应用EAR中所包含的commons-logging,所以Classloader类加 载内存泄漏就有可能出现了。问题简单分析如下:

1) 我们一般在应用中使用commons-logging的API来获得Log:protected final Log logger = LogFactory.getLog (getClass())。

2) 为此我们分析commons-logging类库中LogFactory类,请注意其中factories 是类静态变量,而getFactory()方法是静态方法,都是属于类属性。
通过下面代码我们可以清晰的得知:如果LogFactory在应用EAR上一级 的 类加载路径中被加载,那么在应用类加载器加载、创建的LogFactory实例(不管 org.apache.commons.logging.impl.LogFactoryImpl还是 org.apache.commons.logging.impl.Log4jFactory),将会被上一级类加载器中的LogFactory类所强制 性地引用并存储在静态变量factories 的类属性中。
故而即使强行停止此EAR应用,但是由于系统类加载器加载的LogFactory中的factories 强制引用了此应用创建的LogFactory实例对象不能被进行垃圾回收,从导致所有的Class无法被销毁,最终形成Classloader类加载内存泄漏。

LogFactory.java

  1. public   abstract   class  LogFactory {  
  2.     protected   static  Hashtable factories =  null ;  
  3.     ……  
  4. public   static  Log getLog(Class clazz)  throws  LogConfigurationException {  
  5.         return  getFactory().getInstance(clazz);  
  6.     }  
  7.     public   abstract  Log getInstance(Class class1)  throws  LogConfigurationException;  
  8. ……  
  9.     public   static  LogFactory getFactory()  throws  LogConfigurationException {  
  10.         ClassLoader contextClassLoader = getContextClassLoader();  
  11.         LogFactory factory = getCachedFactory(contextClassLoader);  
  12.         if  (factory !=  null )  
  13.             return  factory;  
  14.             ……  
  15.             //下面大量的代码被删除,主要用于:创建由应用加载的新LogFactory 对象 factory   
  16.             ……  
  17.         if  (factory !=  null ) {  
  18.             cacheFactory(contextClassLoader, factory);  
  19.              ……  
  20.         }  
  21.         return  factory;  
  22.     }  
  23.     private   static   void  cacheFactory(ClassLoader classLoader, LogFactory factory) {  
  24.         if  (factory !=  null )  
  25.             if  (classLoader ==  null )  
  26.                 nullClassLoaderFactory = factory;  
  27.             else   
  28.                 factories.put(classLoader, factory);  
  29.     }  
  30. ……  
  31. }  

public abstract class LogFactory {     protected static Hashtable factories = null;     …… public static Log getLog(Class clazz) throws LogConfigurationException {         return getFactory().getInstance(clazz);     }     public abstract Log getInstance(Class class1) throws LogConfigurationException; ……     public static LogFactory getFactory() throws LogConfigurationException {         ClassLoader contextClassLoader = getContextClassLoader();         LogFactory factory = getCachedFactory(contextClassLoader);         if (factory != null)             return factory;             ……             //下面大量的代码被删除,主要用于:创建由应用加载的新LogFactory 对象 factory             ……         if (factory != null) {             cacheFactory(contextClassLoader, factory);              ……         }         return factory;     }     private static void cacheFactory(ClassLoader classLoader, LogFactory factory) {         if (factory != null)             if (classLoader == null)                 nullClassLoaderFactory = factory;             else                 factories.put(classLoader, factory);     } …… }

d) 把log4j类库放置到系统类路径下(比如:JVM、WebSphere Extensions Class loader、WebSphere lib/app Class loader、WebSphere "server" Class loader类路径),并且使用log4j的“Context Repository Selector”模式来获得各个应用的logging配置。
如果此时应用EAR/WAR中包含log4j类库将会出现Class Cast Exceptions异常不能正常运行;如果应用EAR/WAR中不包含log4j类库,虽然应用能够正常运行但是会导致Classloader类加载内 存泄漏。关于log4j的“Context Repository Selector”模式请参考http://www.qos.ch/logging/sc.jsp

e) 如果你开发的组件中使用了java.beans.Introspector来进行Class/Method MetaData的缓存,可能会引发Classloader类加载内存泄漏。
每次解析Java Bean 的属性、方法是比较耗CPU资源的,所以在很多的框架级组件如Spring中普遍使用java.beans.Introspector来Cache缓存JavaBean的定义,Introspector类中会使用private static Map beanInfoCache = Collections.synchronizedMap (new WeakHashMap())类静态变量的方式来进行保存JavaBean的定义。
而Introspector是由系统JVM ClassLoader进行加载的,所以应用中定义的JavaBean Class将会被系统类加载器加载的Introspector强制引用,从而导致在应用被停止的状态下,所有与此应用相关的类无法被回收。
Introspector具体代码如下:

java.beans.Introspector.java

  1. package  java.beans;  
  2. ……  
  3. public   class  Introspector {  
  4. ……  
  5.     private   static  Map declaredMethodCache = Collections.synchronizedMap( new  WeakHashMap());  
  6.     private   static  Map beanInfoCache = Collections.synchronizedMap( new  WeakHashMap());  
  7. ……  
  8.     public   static  BeanInfo getBeanInfo(Class<?> beanClass)  throws  IntrospectionException  
  9.     {  
  10.         if  (!ReflectUtil.isPackageAccessible(beanClass)) {  
  11.             return  ( new  Introspector(beanClass,  null , USE_ALL_BEANINFO)).getBeanInfo();  
  12.         }  
  13.         BeanInfo bi = (BeanInfo)beanInfoCache.get(beanClass);  
  14.        if  (bi ==  null ) {  
  15.            bi = (new  Introspector(beanClass,  null , USE_ALL_BEANINFO)).getBeanInfo();  
  16.            beanInfoCache.put(beanClass, bi);  
  17.        }  
  18.        return  bi;  
  19.   }  
  20. ……  
  21. }  

package java.beans; …… public class Introspector { ……     private static Map declaredMethodCache = Collections.synchronizedMap(new WeakHashMap());     private static Map beanInfoCache = Collections.synchronizedMap(new WeakHashMap()); ……     public static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException     {         if (!ReflectUtil.isPackageAccessible(beanClass)) {             return (new Introspector(beanClass, null, USE_ALL_BEANINFO)).getBeanInfo();         }         BeanInfo bi = (BeanInfo)beanInfoCache.get(beanClass);        if (bi == null) {            bi = (new Introspector(beanClass, null, USE_ALL_BEANINFO)).getBeanInfo();            beanInfoCache.put(beanClass, bi);        }        return bi;   } …… }

我们同样可以在Spring org.springframework.beans.CachedIntrospectionResults类的注释中,清晰的得知Spring中可能会存在Introspection Classloader类加载内存泄漏: Internal class that caches JavaBeans {@link java.beans.PropertyDescriptor} information for a Java class. Not intended for direct use by application code. Necessary for own caching of descriptors within the application's ClassLoader, rather than rely on the JDK's system-wide BeanInfo cache (in order to avoid leaks on ClassLoader shutdown).
在CachedIntrospectionResults中同样使用了类静态变量classCache来缓存类的定义,如果Spring的类库存在于应用类加载器上一级 的JVM系统或应用服务器类路径上,则有可能导致Classloader类加载内存泄漏。
CachedIntrospectionResults具体代码如下:

org.springframework.beans.CachedIntrospectionResults.java(注:针对Spring2.0.7以后的版本

  1. package  org.springframework.beans;  
  2. ......  
  3. final   class  CachedIntrospectionResults  
  4. {  
  5.     private   static   final  Log logger;  
  6.     private   static   final  Map classCache = Collections.synchronizedMap( new  WeakHashMap());  
  7.     private   final  BeanInfo beanInfo;  
  8.     private   final  Map propertyDescriptorCache;  
  9.     static   
  10.     {logger = LogFactory.getLog(org.springframework.beans.CachedIntrospectionResults.class );  }  
  11.     public   static  CachedIntrospectionResults forClass(Class beanClass)  
  12.         throws  BeansException  
  13.     {  
  14.         CachedIntrospectionResults results = null ;  
  15.         Object value = classCache.get(beanClass);  
  16.    ……  
  17.         if (results ==  null )  
  18.         {  
  19.             results = new  CachedIntrospectionResults(beanClass);  
  20.             boolean  cacheSafe = isCacheSafe(beanClass);  
  21.             if (logger.isDebugEnabled())  
  22.                 logger.debug("Class ["  + beanClass.getName() +  "] is "  + (cacheSafe ?  ""  :  "not " ) +  "cache-safe" );  
  23.             if (cacheSafe)  
  24.                 classCache.put(beanClass, results);  
  25.             else   
  26.                 classCache.put(beanClass, new  WeakReference(results));  
  27.         } else   
  28.         if (logger.isDebugEnabled())  
  29.             logger.debug("Using cached introspection results for class ["  + beanClass.getName() +  "]" );  
  30.         return  results;  
  31. }  
  32.     private  CachedIntrospectionResults(Class clazz) throws  BeansException  
  33.     {  
  34.         ……  
  35. beanInfo = Introspector.getBeanInfo(clazz);  
  36.             Class classToFlush = clazz;  
  37.         ……  
  38. }  
  39. ……  
  40. }  

package org.springframework.beans; ...... final class CachedIntrospectionResults {     private static final Log logger;     private static final Map classCache = Collections.synchronizedMap(new WeakHashMap());     private final BeanInfo beanInfo;     private final Map propertyDescriptorCache;     static     {logger = LogFactory.getLog(org.springframework.beans.CachedIntrospectionResults.class);  }     public static CachedIntrospectionResults forClass(Class beanClass)         throws BeansException     {         CachedIntrospectionResults results = null;         Object value = classCache.get(beanClass);    ……         if(results == null)         {             results = new CachedIntrospectionResults(beanClass);             boolean cacheSafe = isCacheSafe(beanClass);             if(logger.isDebugEnabled())                 logger.debug("Class [" + beanClass.getName() + "] is " + (cacheSafe ? "" : "not ") + "cache-safe");             if(cacheSafe)                 classCache.put(beanClass, results);             else                 classCache.put(beanClass, new WeakReference(results));         } else         if(logger.isDebugEnabled())             logger.debug("Using cached introspection results for class [" + beanClass.getName() + "]");         return results; }     private CachedIntrospectionResults(Class clazz)throws BeansException     {         …… beanInfo = Introspector.getBeanInfo(clazz);             Class classToFlush = clazz;         …… } …… }

f) 在commons-beanutils 1.7版本(包括1.7版本)的组件中存在Classloader类加载内存泄漏,只有最新的1.8.0Beta修正了此潜在的问题,
问题描述:
* [BEANUTILS-59] - Memory leak on webapp undeploy in WrapDynaClass
* [BEANUTILS-156] - Memory leak on webapp undeploy in MappedPropertyDescriptor
详细描述请参考:http://commons.apache.org/beanutils/v1.8.0-BETA/RELEASE-NOTES.txt

g) 在应用中使用了commons-beanutils 的MethodUtils来对类的方法Method进行操作,那同样存在Classloader类加载内存泄漏的可能。
如果commons-beanutils类库放置在应用上一级 的类加载路径中,并且有其他应用(或系统代码)在此应用之前 使用同样方式MethodUtils来对Class的Method进行操作(在其他类加载器上加载MethodUitls),那么Classloader类加载内存泄漏必然出现。我们可以参考MethodUtils对应代码,可以非常直观地定位问题:

  1. package  org.apache.commons.beanutils;  
  2. ……  
  3. public   class  MethodUtils  
  4. {  
  5. private   static  WeakHashMap cache =  new  WeakHashMap();  
  6. ……  
  7.     public   static  Method getAccessibleMethod(Class clazz, String methodName, Class parameterTypes[])  
  8.     {  
  9.         MethodDescriptor md;  
  10.         Method method;  
  11.         md = new  MethodDescriptor(clazz, methodName, parameterTypes,  true );  
  12.         method = (Method)cache.get(md);  
  13.         if (method !=  null )  
  14.             return  method;  
  15.         try {  
  16.             method = getAccessibleMethod(clazz.getMethod(methodName, parameterTypes));  
  17.             cache.put(md, method);  
  18.             return  method;  
  19.         }catch (NoSuchMethodException e){  
  20.             return   null ;  
  21.     }  
  22. ……  
  23. }  

package org.apache.commons.beanutils; …… public class MethodUtils { private static WeakHashMap cache = new WeakHashMap(); ……     public static Method getAccessibleMethod(Class clazz, String methodName, Class parameterTypes[])     {         MethodDescriptor md;         Method method;         md = new MethodDescriptor(clazz, methodName, parameterTypes, true);         method = (Method)cache.get(md);         if(method != null)             return method;         try{             method = getAccessibleMethod(clazz.getMethod(methodName, parameterTypes));             cache.put(md, method);             return method;         }catch(NoSuchMethodException e){             return null;     } …… }

h) 如果应用中使用到Java 1.5语法定义的 enum 类,而此定义的类放置在应用上一级 的类加载路径中。首先在我们开发的 应用类加载器中加载并初始化了应用中定义的enum类,随后其他应用EAR/WAR(或系统代码)也使用到此定义的enum类,在并把此类enum属性引 用放置到(针对其他应用的)类静态变量或Servlet类变量,那么我们开发应用的Classloader类加载器将不会被回收,最终内存泄漏必然出现。 举例如下:

OperationEnum.java

  1. package  com.test.enumeration;  
  2. public   enum  OperationEnum {  
  3.     QUOTE(1 ), ISSUE( 2 ), RENEW( 4 ), CANCEL( 12 ),  
  4.     ENDORSE(16 ), CHANGE( 64 ), REINSTATE( 192 );  
  5.     private   int  operation =  0 ;  
  6.     private  OperationEnum( int  op) {  
  7.         this .operation = op;  
  8.     }  
  9.     public   boolean  isNewOperation() {  
  10.         return  ( this .operation== 2 ) ||  
  11.                (this .operation== 4 ) ||  
  12.                (this .operation== 192 );  
  13.     }  
  14. }  

package com.test.enumeration; public enum OperationEnum {     QUOTE(1), ISSUE(2), RENEW(4), CANCEL(12),     ENDORSE(16), CHANGE(64), REINSTATE(192);     private int operation = 0;     private OperationEnum(int op) {         this.operation = op;     }     public boolean isNewOperation() {         return (this.operation==2) ||                (this.operation==4) ||                (this.operation==192);     } }

其他EAR 应用中 使用到上面定义enum类的样例代码

  1. public   class  LeakCauseServletInOtherApp  extends  HttpServlet {  
  2.     private   final  OperationEnum operation = OperationEnum.CHANGE;  //   
  3.     protected   void  doGet(HttpServletRequest request, HttpServletResponse response)  throws  ServletException, IOException {  
  4.         doSomething(request, response);  
  5.     }  
  6.     ……  
  7. }  

public class LeakCauseServletInOtherApp extends HttpServlet {     private final OperationEnum operation = OperationEnum.CHANGE; //     protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {         doSomething(request, response);     }     …… }

i) 导致Classloader 类加载内存泄漏的另外一个重要因素就是 :如果在框架中或应用使用ThreadLocal线程数据空间来存储实例对象,你必须知道在WAS等应用服务器中线程实例都是属于池态的,是由应用服务器WebContainer等容器来维护这些线程实例。
即使应用被停止了,这些池态的线程实例仍然属于存活运行状态,如果应用Web Servlet线程运行过程中在ThreadLocal上存储的实例对象没有被正确删除,可能导致线程类加载内存泄漏问题。
在老版本的DOM4J 、Mozilla Rhino、CGLIB 都存在这种类型的线程内存泄漏,请使用这些组件的最新版本来避免此类泄漏问题的发生。

1) Hibernate 3.2.2版本中存在ThreadLocal 线程变量内存泄漏问题,在3.2.3版本中得到修订。详细内容请参考“Big memory leak in the use of CGLIB” http://opensource.atlassian.com/projects/hibernate/browse/HHH-2481

2) CGLIB 2.1存在ThreadLocal 线程变量内存泄漏问题,在最新的版本2.1_3中问题应该得到修订。详细内容请参考
http://sourceforge.net/tracker/index.php?func=detail&aid=1257327&group_id=56933&atid=482368
http://sourceforge.net/tracker/index.php?func=detail&aid=1291183&group_id=56933&atid=482370
http://jira.jboss.com/jira/browse/JBAS-2256

3) dom4j 1.6之前的版本存在ThreadLocal 线程变量内存泄漏问题,在1.6以后的版本中此问题得到解决。
问题描述:https://sourceforge.net/tracker/index.php?func=detail&aid=1070309&group_id=16035&atid=116035
Bug修订描述:“Added a SingletonStrategyclass for managing singletons. This allows to use different strategies for singletons, like: one instance per VM, one instance per thread, ... This change removed the usage of ThreadLocals.”
http://www.dom4j.org/changes-report.html

ClassLoader 类加载内存泄漏问题的解决方案

ClassLoader类加载内存泄漏问题解决的基本原则:

1、 不要把应用使用的类库放置到JRE或WebSphere服务器的类加载器路径中,尽量把使用的类库保持在EAR 或WAR/WEB-INF/Lib路径中。

2、 尽量在WebSphere服务器中设置类加载顺序为“Child-First ClassLoaders”/“Parent-Last ClassLoaders”。

3、 针对DOM4J、Mozilla Rhino、CGLIB请确认使用最新的版本,并确认类库保存在应用EAR级别之下。

4、 尽量避免使用Java 1.5语法定义的 enum 类,如果使用了enum类,必须确认开发的类库保持在应用EAR类加载器这一级别之下,而千万不能放置到WebSphere或JVM类库路径中。

5、 使用最新版本的commons-logging,并确认类库保存在应用EAR级别之下。

6、 使用最新版本的commons-beanutils,并确认类库保存在应用EAR级别之下,千万不能放置到WebSphere或JVM类库路径中。

7、 使用最新版本的log4j,并确认类库保存在应用EAR级别之下,千万不能放置到WebSphere或JVM类库路径中。

8、 不要在生产环境中使用DriverManager。

9、 不要在生产环境中使用commons-dbcp作为数据源实现,推荐使用应用服务器提供的数据源。

10、 不要在应用中使用java.util.logging.Level。

由于无法避免commons-logging类库存在于WebSphere应用服务器类路径和大量J2EE OpenSource组件使用java.beans.Introspector来Cache缓存JavaBean定义的事实,针对这种情况我们如何来应对和处理呢?

就像我们上面为了再现Class重复加载问题而编写的ClassLoaderTestServlet样例中,使用最简单的方式调用有Spring管 理的类实例对象StaticClass sc,就发生了臭名昭著的Class类加载内存泄漏问题。我们如何来避免此问题的发生?

针对 java.beans.Introspector 内存泄漏问题

其实在Spring框架2.0.7 以后的版本中已经对此有了对应的解决方案,提供了一个专门处理 Java.beans.Introspector内存泄漏问题的辅助类: org.springframework.web.util.IntrospectorCleanupListener,并附有专门的文档进行说明说明。 文档原话如下:

Listener that flushes the JDK's JavaBeans Introspector cache on web app shutdown. Register this listener in your web.xml to guarantee proper release of the web application class loader and its loaded classes.


If the JavaBeans Introspector has been used to analyze application classes, the system-level Introspector cache will hold a hard reference to those classes. Consequently, those classes and the web application class loader will not be garbage-collected on web app shutdown! This listener performs proper cleanup, to allow for garbage collection to take effect.

Unfortunately, the only way to clean up the Introspector is to flush the entire cache, as there is no way to specifically determine the application's classes referenced there. This will remove cached introspection results for all other applications in the server too.

Note that this listener is not necessary when using Spring's beans infrastructure within the application, as Spring's own introspection results cache will immediately flush an analyzed class from the JavaBeans Introspector cache and only hold a cache within the application's own ClassLoader. Although Spring itself does not create JDK Introspector leaks, note that this listener should nevertheless be used in scenarios where the Spring framework classes themselves reside in a 'common' ClassLoader (such as the system ClassLoader ). In such a scenario, this listener will properly clean up Spring's introspection cache.

Application classes hardly ever need to use the JavaBeans Introspector directly, so are normally not the cause of Introspector resource leaks. Rather, many libraries and frameworks do not clean up the Introspector: e.g. Struts and Quartz.

Note that a single such Introspector leak will cause the entire web app class loader to not get garbage collected! This has the consequence that you will see all the application's static class resources (like singletons) around after web app shutdown, which is not the fault of those classes!

This listener should be registered as the first one in web.xml , before any application listeners such as Spring's ContextLoaderListener. This allows the listener to take full effect at the right time of the lifecycle.

在上面的文档中,我们可以清晰的得知,如果把Spring类库放置到JVM系统或应用服务器一级别的类库路径中,我们必须在web.xml中配置 org.springframework.web.util.IntrospectorCleanupListener,才能防止Spring中可能存在 的Introspector内存泄漏。

web.xml样例配置如下:

web.xml配置文件 ( Spring Listener 只有在Spring2.0 以后的版本才存在

  1. <? xml   version = "1.0"   encoding = "UTF-8" ?>   
  2. < web-app   id = "WebApp_ID"   version = "2.4"   
  3.     xmlns = "http://java.sun.com/xml/ns/j2ee"   
  4.     xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"   
  5.     xsi:schemaLocation = "http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" >   
  6.     < display-name > ClassLoader </ display-name >   
  7.     < context-param >   
  8.         < param-name > contextConfigLocation </ param-name >   
  9.         < param-value > classpath*:spring/*.xml </ param-value >   
  10.     </ context-param >   
  11.     <!-- Spring 刷新Introspector防止内存泄露,推荐把此Listener放置在第一个,至少是Spring相关 Listener的第一个 -->   
  12.     < listener >   
  13.         < listener-class > org.springframework.web.util.IntrospectorCleanupListener </ listener-class >   
  14.     </ listener >   
  15.     < listener >   
  16.         < listener-class > org.springframework.web.context.ContextLoaderListener </ listener-class >   
  17.     </ listener >   
  18. ……  

<?xml version="1.0" encoding="UTF-8"?> <web-app id="WebApp_ID" version="2.4"     xmlns="http://java.sun.com/xml/ns/j2ee"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"     xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">     <display-name>ClassLoader</display-name>     <context-param>         <param-name>contextConfigLocation</param-name>         <param-value>classpath*:spring/*.xml</param-value>     </context-param>     <!-- Spring 刷新Introspector防止内存泄露,推荐把此Listener放置在第一个,至少是Spring相关 Listener的第一个 -->     <listener>         <listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>     </listener>     <listener>         <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>     </listener> ……

org.springframework.web.util.IntrospectorCleanupListener样例代码如下:

IntrospectorCleanupListener.java (为Spring 2.0.7 以后版本的代码 )

  1. package  org.springframework.web.util;  
  2. ……  
  3. public   class  IntrospectorCleanupListener  implements  ServletContextListener {  
  4.     public   void  contextInitialized(ServletContextEvent event) {  
  5.         CachedIntrospectionResults.acceptClassLoader(Thread.currentThread().getContextClassLoader());  
  6.     }  
  7.     public   void  contextDestroyed(ServletContextEvent event) {  
  8.         CachedIntrospectionResults.clearClassLoader(Thread.currentThread().getContextClassLoader());  
  9.         Introspector.flushCaches();  
  10.     }  
  11. }  

package org.springframework.web.util; …… public class IntrospectorCleanupListener implements ServletContextListener {     public void contextInitialized(ServletContextEvent event) {         CachedIntrospectionResults.acceptClassLoader(Thread.currentThread().getContextClassLoader());     }     public void contextDestroyed(ServletContextEvent event) {         CachedIntrospectionResults.clearClassLoader(Thread.currentThread().getContextClassLoader());         Introspector.flushCaches();     } }

针对 WebSphere 应用服务器类路径中存在 commons-logging 类库,应用中 commons-logging 的使用导致 ClassLoader 类加载内存泄漏问题

其实针对上面编写ClassLoaderTestServlet样例的EAR应用,我们在测试过程中并没有把Spring类库放置到 WebSphere应用服务器或JVM系统类库路径中,Spring类库仅仅存在于应用的WEB-INF/lib目录中(即:应用的类加载范围内),那为 什么还出现类加载内存泄漏?应该不是由Introspector内存泄漏问题引起的!

通过分析Spring源代码得知,Spring Bean定义加载诸如ClassPathXmlApplicationContext等类的父类AbstractApplicationContext中 使用了commons-logging组件来进行框架日志记录,所以ClassLoaderTestServlet样例测试中的内存泄漏是由 commons-logging导致的。

那么,我们如何避免commons-logging内存泄漏?

其实我们可以仿照上面Spring 框架中针对Introspector泄漏问题的解决方案,编写一个ServletContextListener来监听Servlet容器的生命周期,一 旦发现WebContainer被终止,我们可以主动释放存储在LogFactory类静态变量factories中所有由此应用产生的类实例对象,最终 解决commons-logging内存泄漏。

用于清除LogFactory类静态变量factories中实例对象的代码如下:

ApplicationLifecycleListener.java

  1. package  com.test;  
  2. import  javax.servlet.ServletContextEvent;  
  3. import  javax.servlet.ServletContextListener;  
  4. import  org.apache.commons.logging.LogFactory;  
  5. public   class  ApplicationLifecycleListener  implements  ServletContextListener {  
  6.     public   void  contextDestroyed( final  ServletContextEvent sce) {  
  7.         LogFactory.release(Thread.currentThread().getContextClassLoader());  
  8.     }  
  9.     public   void  contextInitialized( final  ServletContextEvent sce) {  
  10.     }  
  11. }  

package com.test; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import org.apache.commons.logging.LogFactory; public class ApplicationLifecycleListener implements ServletContextListener {     public void contextDestroyed(final ServletContextEvent sce) {         LogFactory.release(Thread.currentThread().getContextClassLoader());     }     public void contextInitialized(final ServletContextEvent sce) {     } }

当然我们必须把此ServletContextListener 注册到web.xml 中,web.xml样例配置如下:

web.xml配置文件

  1. <? xml   version = "1.0"   encoding = "UTF-8" ?>   
  2. < web-app   id = "WebApp_ID"   version = "2.4"   
  3.     xmlns = "http://java.sun.com/xml/ns/j2ee"   
  4.     xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"   
  5.     xsi:schemaLocation = "http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" >   
  6.     < display-name > ClassLoader </ display-name >   
  7.     < context-param >   
  8.         < param-name > contextConfigLocation </ param-name >   
  9.         < param-value > classpath*:spring/*.xml </ param-value >   
  10.     </ context-param >   
  11.     <!-- Spring 刷新Introspector防止内存泄露,推荐把此Listener放置在第一个,至少是Spring相关 Listener的第一个 -->   
  12.     < listener >   
  13.         < listener-class > org.springframework.web.util.IntrospectorCleanupListener </ listener-class >   
  14.     </ listener >   
  15.     < listener >   
  16.         < listener-class > org.springframework.web.context.ContextLoaderListener </ listener-class >   
  17.     </ listener >   
  18.     < listener >   
  19.         < listener-class > com.test.ApplicationLifecycleListener </ listener-class >   
  20.     </ listener >   
  21. ……  

<?xml version="1.0" encoding="UTF-8"?> <web-app id="WebApp_ID" version="2.4"     xmlns="http://java.sun.com/xml/ns/j2ee"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"     xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">     <display-name>ClassLoader</display-name>     <context-param>         <param-name>contextConfigLocation</param-name>         <param-value>classpath*:spring/*.xml</param-value>     </context-param>     <!-- Spring 刷新Introspector防止内存泄露,推荐把此Listener放置在第一个,至少是Spring相关 Listener的第一个 -->     <listener>         <listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>     </listener>     <listener>         <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>     </listener>     <listener>         <listener-class>com.test.ApplicationLifecycleListener</listener-class>     </listener> ……

通过实验,我们最终解决了Spring标准应用中臭名昭著的ClassLoader类加载器内存泄漏问题。!


参考资料:

Tomcat和Websphere类加载机制

http://gocom.primeton.com/modules/newbb/item42595_42595.htm

JVM的垃圾回收机制详解和调优

http://www.builder.com.cn/2007/0824/468927.shtml

With updating ClassLoader several times, jdk1.4.1_05 server VM will be down with an error java.lang.OutOfMemoryError

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4957990

java.lang.OutOfMemoryError: PermGen space及其解决方法

http://www.wujianrong.com/archives/2006/12/javalangoutofmemoryerror_permg.html

OutOfMemory error when repetatively deploying and undeploying with 10 minute interval in JBoss Application Server

http://jira.jboss.com/jira/browse/JBAS-2299

How to fix the dreaded "java.lang.OutOfMemoryError: PermGen space" exception (classloader leaks)

http://blogs.sun.com/fkieviet/entry/how_to_fix_the_dreaded

Commons-logging Logging/UndeployMemoryLeak

http://wiki.apache.org/jakarta-commons/Logging/UndeployMemoryLeak?action=print

Supporting the log4j RepositorySelector in Servlet Containers

http://www.qos.ch/logging/sc.jsp

commons-logging Classloader and Memory Management

http://commons.apache.org/logging/guide.html#Classloader and Memory Management

Big memory leak in the use of CGLIB

http://opensource.atlassian.com/projects/hibernate/browse/HHH-2481

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值