内存泄漏 - 从Class类加载器说起

一起学习,一起进步,做积极的人!

背景

某公司技术人员针对企业应用系统12月10日内存溢出事件进行了广泛的技术探讨,并得到了一些建设性的建议和结论。

其中重点提到了:

“关于websphere在控制台中进行重启应用,而不是重启整个websphere,这时候静态类是不能回收的,造成些类不能销毁,占用着内存,而且这些内存是不能重复使用的,可以说是内存泄露。静态的类不能销毁,那么静态类引用的对象也不能销毁,因此一些bean都不能被正常回收,其实这些小对象占用内存是很少的,最主要的是这些类引用的缓存没有销毁,这些缓存才是占用内存的大头。如果系统用了一两天,然后有人在控制台上将这个应用重启,那么缓存将不能销毁,造成大量内存浪费,因此现在我们分析的dump文件中缓存一半的内存是由这些没有销毁的无用的缓存占用的。其实BSP中有个HttpServletContextListener,这个监听器能够在关闭应用的时候清空缓存,但是从dump文件中可以看出这个监听器可能没有在应用关闭的时候调用。对于静态类支持有的对象销毁问题需要进行研究,解决Websphere的这种bug。”

package com.test;  
public class StaticCls{  
public static void main(String[] args){  
    OuterCls.InnerCls.test();  
  }  
}  
class OuterCls{  
  public static class InnerCls{  
     public static void test(){  
        System.out.println("InnerCls");  
    }  
  }  
}

所以大家认为是WAS的Bug导致:在进行应用重新启动过程中,一些类的Class MetatData本身描述不能从内存中被释放,同时在系统中一些系统类型的类Class进行了大量类静态变量的定义,并在这些类静 态变量中存放了大量的对象。长此以往,多次重新启动应用从而大量的内存被占用,最终导致内存泄漏。

针对Java静态类的补充说明:通常一个普通类不允许声明为静态的,只有一个内部类才可以。在一个内部类中如果想提供静态方法访问的前提下,我们才会把此内部类设置为静态类。这时不需实例一个外部类和内部类,而可以直接调用内部内的静态方法。样例如下:

package com.test;  
public class StaticCls {  
   public static void main(String[] args) { 
   OuterCls.InnerCls.test();  
   }  
}  
class OuterCls {  
  public static class InnerCls {  
    public static void test() {  
      System.out.println("InnerCls");  
    }  
  }  
}

相信在应用系统中这种静态类的应用场景并不多见,所以上面提到的静态类的说法并不准确,应该改正为:类静态变量。

虽然把“静态类”改变为“类静态变量”,但是上面提到的Class MetatData类本身描述不能从内存中被释放的问题确实存在。在周恒总12月14日询问HeapAnalyzer问题邮件中有一张图片能非常直观的说 明问题,实际生产环境HeapDump的分析文件图示如下:

其中class org/loushang/bsp/organization/domain/support/Stru描述的是这个类Stru本身的属性:

a) 其中Size (304)是描述类Stru本身的大小

b) 其中No.Child (43)描述的是Stru类中所有变量引用到和方法中使用到的类的个数

c) 其中 TotalSize (348,544,600)描述的是此Stru类中所有引用到和方法中使用到的类的大小 + 所有引用到和方法中使用到的类实例化对象的大小,所有值比较大。但是仅仅通过上图中罗列的TotalSize (348,544,600)并不能直接说明内存使用异常根源来自于Stru

在给某总的电子邮件回复中,我提到:“在图中存在两个不同地址、不同大小的class org/loushang/bsp/organization/domain/support/Stru实属有些怪异。一般情况下(在正常类加载器运行过程中)在当前内存中只会存在一份Class类的描述。”

当时我仅仅是觉得比较怪异:为什么在内存中出现了两份Stru类的Class描述?但是没有引起足够重视:认为它是一个严重问题。

在后续某总的邮件中提到:

“经我们的技术人员测试,发现两个class的问题是一个Websphere的bug,重启动ear应用后静态变量及其引用的对象不会被释放。每重启一次就多一个class行。”

随后某总邮件12月19日邮件中在此提到:

“通过控制台重启的静态变量不释放。Was是有意这么做的,还是这个结论不正确?”

至此,形成了一个命题:

在WAS服务器中,如果重启J2EE应用(不重启WAS服务器),某些类型的类不能从内存中被回收。多次重启应用可能会导致内存泄漏?

这是不是WAS的一个Bug?

疑问:应用重启,导致内存泄漏?针对这个疑问,我们可以求助于Google获得一些线索,我们可以通过检索关键词:OutOfMemoryError redeploy,来获得关于重启应用导致内存泄漏的大量信息。

我们把其中几个比较经典的来分享一下:

1、 在JBOSS服务器中重复部署启动应用,会导致OutOfMemory URL: http://jira.jboss.com/jira/browse/JBAS-2299 描述:OutOfMemory error when repetatively deploying and undeploying with 10 minute interval 问题仍然没有解决

2、 为什么重复部署应用时会导致Tomcat内存不断使用增加?URL: http://wiki.apache.org/tomcat/FAQ/Deployment 描述:Why does the memory usage increase when I redeploy a web application? 问题仍然没有解决

3、 SUN JDK+Tomcat 5.5.20运行服务的时候遇到问题,重启应用,服务器几天后就会挂掉,并报java.lang.OutOfMemoryError: PermGen space异常。URL: http://www.javaeye.com/topic/80620 描述:推断可能是由于SUN JDK 的BUG引起加载到SUN JVM Perm区域的Class不能被回收导致内存泄漏,推荐使用IBM JDK 或 BEA JRokit虚拟机解决问题。(我的评论:其实不是JVM BUG导致的)

4、 认为是Spring导致OutOfMemory,展开大讨论 URL: http://forum.springframework.org/showthread.php?t=21383&highlight=cglib+cache&page=4

5、 有的认为是SUN JDK Bug引起的 URL: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4957990 描述:2003年的时候就有一个bug报告给sun,但是到现在,这个bug还没有close!有人在这个bug加了句评语:“A bug this critical is open since 2003? Absolutely shameful.”大家觉得SUN在这个BUG上确实有些丢脸,其实SUN并不认为这是JVM本身BUG引起的,一定是应用本身的BUG导致的。

真是众说纷纭,并没有一个确切的答案。

但是我们可以认定此问题比较普遍,看上去并不是由于WAS应用服务器、JDK/JRE、Tomcat、Spring、Hibernate等中间件Bug引起的,下面会来论述一下我们猜想:

1、 针对Class的MetaData类描述在SUN JDK中是由专门的内存空间PermGen space来存储的(PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,这块内存主要是被JVM存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space中,它和存放类实例(Instance)的Heap区域不同),而PermGen空间缺省比较小为4M。所以一旦出现应用重新启动并存在相同Class重复加载的情况,极易导致PermGen溢出,从而直接导致“java.lang.OutOfMemoryError: PermGen space”的出现。换用IBM JDK或BEA JRokit JVM看似解决问题,其实并没有根本解决Class类重复加载的问题。只不过在IBM JDK或BEA JRokit JVM中并没有专门的PermGen空间来存放Class类描述,而是与JVM Heap共用空间,所以重复加载的Class并不能马上导致内存溢出。但是日积月累,问题仍然会显现出来,就像某应用系统问题一般。

2、 为什么同样的问题,在不同J2EE平台、不同J2EE框架、不同JDK都同样出现?看上去并不像是由这些中间件Bug导致,难道这些不同厂商、开发人员开发的代码存在同样的Bug?

真是事实胜于雄辩,我们还是用事实来说话吧:我们想办法开发一些场景来再现这个问题。

问题的再现 如何判断在重启应用后出现Class类重复加载的问题?

针对Class是否被重复加载的这个问题,市面上的所有JVM Profiling诊断工具都无法进行有效的跟踪和调试。目前唯一可行的方式:就是使用IBM JVM运行存在问题的应用,通过JVM接口或Unix环境中kill -3 <Java_PID>的方式让JVM产生当前JVM HeapDump文件,据此我们可以使用IBM HeapAnalyzer工具来分析是否存在Class类重复加载的问题。

为了简化产生Java HeapDump的过程,我们专门开发了用于产生HeapDump文件的JSP页面,以方便我们在Windows平台的测试和验证。

dump.jsp

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">  
<%@page language="java" contentType="text/html; charset=GB18030" pageEncoding="GB18030"%>  
<html><head>  
<title>dump</title>  
<meta http-equiv="Content-Type" content="text/html; charset=GB18030"></head>  
<body>  
<h2> 产生HeapDump 和 JavaCore</h2>  
<%String heapdumpCmd = request.getParameter("heapdump");  
if(heapdumpCmd!=null) com.ibm.jvm.Dump.HeapDump();  
String javacoreCmd = request.getParameter("javacore");  
if(javacoreCmd != null) com.ibm.jvm.Dump.JavaDump();  
String gcCmd = request.getParameter("gc");  
if(gcCmd != null) System.gc();%>  
<form action="dump.jsp">  
<input type="submit" name="gc" value="GarbageCollection">  
<input type="submit" name="heapdump" value="CreateHeapDump">  
<input type="submit" name="javacore" value="CreateJavaCore"></form>  
</body>  
</html>

尝试编写样例再现Class重复加载的问题

根据某企业开发中心和其他团队技术研发中心的反馈,一直认为是由于类静态变量的采用导致Class无法被释放,从而出现Class重复加载的问题。为此我们模拟以下代码:

ClassLoadBugServlet.java

public class ClassLoadBugServlet extends javax.servlet.http.HttpServlet implements javax.servlet.Servlet{  
    private static byte[] testByteArray = newbyte[2024000];  
    public ClassLoadBugServlet(){  
        super();  
    }  
    protected void doGet(HttpServletRequest request,HttpServletResponse response) throwsServletException,IOException{  
        perform(request,response);  
    }  
    protected void doPost(HttpServletRequest request,HttpServletResponse response) throwsServletException,IOException{  
        perform(request,response);  
    }  
    private void perform(HttpServletRequest request,HttpServletResponse response) throwsIOException{  
        StaticClasss c = newStaticClass();  
        response.getOutputStream().print(sc.test());  
        System.out.println(sc.test());  
    }  
}

StaticClass.java

public class StaticClass{  
    privates tatic byte[] testByteArray = new byte[2024000];  
    public String test(){  
        return "TestClassLoader";  
    }  
}
public class StaticClass {  
 private static byte[] testByteArray = new byte[2024000];  
 public String test(){ return "Test Class Loader"; }  
}

使用以上代码,我们部署到WAS进行对应的测试,重复运行、重新启动应用数十次,使用上面的dump.jsp产生我们所需要的JVM HeapDump,然后使用IBM HeapAnalyzer进行分析,并没有出现我们上面提到的Class重复加载的问题。

实验一度陷入困境。

Class重复加载问题得以再现

根据某系统和其他系统系统核心框架构建在Spring平台之上,并在Internet网站上有网页反映Spring平台存在Class重复加载的问题http://forum.springframework.org/showthread.php?t=21383&highlight=cglib+cache&page=4

为此我们把上面的样例进行了改造,使用Spring框架来加载StaticClass,进一步验证是否存在Class重复加载的问题。

ClassLoaderTestServlet.java

public class ClassLoaderTestServlet extends javax.servlet.http.HttpServlet implements javax.servlet.Servlet{  
    public ClassLoaderTestServlet(){super();}  
    protected void doGet(HttpServletRequest request,HttpServletResponse response) throwsServletException,IOException{  
        perform(request,response);  
    }  
    protected void doPost(HttpServletRequest request,HttpServletResponse response) throwsServletException,IOException{  
        perform(request,response);  
    }  
    private void perform(HttpServletRequest request,HttpServletResponse response) throwsIOException{  
        StaticClasss c = (StaticClass)getClassPathApplicationContext().getBean("staticClass");  
        response.getOutputStream().print(sc.test());  
        System.out.println(sc.test());  
    }  
    private ApplicationContext getWebApplicationContext(){  
        WebApplicationContextwac=null;  
        wac=(WebApplicationContext)getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);  
        return wac;  
    }  
    private ApplicationContext getClassPathApplicationContext(){  
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/serviceContext.xml");  
        return context;  
    }  
}
public class ClassLoaderTestServlet extends javax.servlet.http.HttpServlet  
implements javax.servlet.Servlet {  
 public ClassLoaderTestServlet() { super(); }  
 protected void doGet(HttpServletRequest request,  
HttpServletResponse response) throws ServletException, IOException  
{ perform(request, response); }  
 protected void doPost(HttpServletRequest request,  
HttpServletResponse response) throws ServletException, IOException  
{ perform(request, response); }  
 private void perform(HttpServletRequest request,  
HttpServletResponse response) throws IOException {  
 StaticClass sc = (StaticClass)  
getClassPathApplicationContext().getBean("staticClass");  
 response.getOutputStream().print(sc.test());  
 System.out.println(sc.test());  
 }  
 private ApplicationContext getWebApplicationContext()  
{ WebApplicationContext wac = null;  
 wac =  
(WebApplicationContext)getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);  
 return wac;  
 }  
 private ApplicationContext getClassPathApplicationContext()  
{ ClassPathXmlApplicationContext context = new  
ClassPathXmlApplicationContext("spring/serviceContext.xml");  
 return context;  
 }  
}

spring/serviceContext.xml

<?xmlversion="1.0"encoding="GBK"?>
<beansxmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-2.0.xsd"
default-autowire="byName"default-lazy-init="true">
<beanid="staticClass"class="com.test.StaticClass"/>
</beans>
<?xml  
version="1.0" encoding="GBK"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
 xmlns:aop="http://www.springframework.org/schema/aop"  
 xsi:schemaLocation="http://www.springframework.org/schema/beans  
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd  
http://www.springframework.org/schema/aop  
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd"  
 default-autowire="byName" default-lazy-init="true">  
 <bean id="staticClass" class="com.test.StaticClass"/>  
</beans>

同样方法我们使用以上代码,部署到WAS进行对应的测试,重复运行、重新启动应用数十次,使用上面的dump.jsp产生我们所需要的JVM HeapDump,然后使用IBM HeapAnalyzer进行分析,最终出现了我们上面提到的Class重复加载的问题。

难道是Spring的Bug导致了Class类重复加载?不可想象?

为了进一步定位在Spring中存在Class重复加载的问题,我们有必要阐述一下JVM内存垃圾回收和Class类加载的基本原理。

JVM GC 垃圾回收机制概述 JVM GC即Java虚拟机垃圾收集机制是指JVM用于释放那些不再使用的对象所占用的内存。Java语言并不要求JVM有GC,也没有规定GC如何工作。不过常用的JVM都有GC,而且大多数GC都使用类似的算法管理内存和执行收集操作。

垃圾收集的目的在于清除不再使用的对象,现在大多数JVM通过采用对象引用遍历的方式(确定对象是否被活动对象引用)来确定是否收集该(垃圾)对 象。对象引用遍历从根线程对象开始,沿着整个对象图上的每条对象引用链接,递归确定可到达(reachable)的对象。如果某对象不能从这些根线程对象 引用到达,则将它作为垃圾收集。

Class类加载的基本机理 像IBM WAS等J2EE应用服务器允许编写的多个J2EE应用EAR/WAR部署到同一台J2EE应用服务器上。如果其中某一个J2EE应用发生改变了,我们只 要针对此EAR/WAR进行更新,重新部署、启动此EAR/WAR应用,并不需要重新启动部署所在的应用服务器,从而不影响部署在同一应用服务器上其他应 用的运行。

这种功能的实现主要是由于在WAS等J2EE服务器中,针对不同应用EAR/WAR提供了不同的ClassLoader类加载器,使用各自的 ClassLoader来加载自身的Class类,故而各个不同EAR/WAR应用之间不会互相影响。简单的来说ClassLoader本身也是一个标准 的Java Class(由J2EE容器提供),只不过其特殊在仅仅用于加载在/WEB-INF/classes或JAR文件中的Class类。正常情况下,当你停止 此应用时,此应用EAR的ClassLoader将会被J2EE应用服务器所丢弃成为垃圾,故而所有由此EAR ClassLoader类加载器所加载的类将会被丢弃成为垃圾,最终会为JVM GC所回收。

而在各个J2EE应用服务器中都存在不同层次的ClassLoader,现我们以WAS 应用服务器为例(其他服务器的ClassLoader请参考《Tomcat和Websphere类加载机制 》):

Websphere类加载机制

Java应用程序运行时,在Class执行和被访问之前,它必须通过类加载器加载使之有效,类加载器是JVM代码的一部分,负责在JVM虚拟机中查 找和加载所有的Java 类和本地的lib库。类加载器的不同配置影响到应用程序部署到应用程序服务器上运行时的行为。JVM和WebSphere应用程序服务器提供了多种不同的 类加载器配置, 形成一个具有父子关系的分层结构。

WebSphere中类加载器的层次结构图示

如上图所示,WebSphere中类加载器被组织成一个自上而下的层次结构,最上层是系统的运行环境JVM,最下层是具体的应用程序,上下层之间形成父子关系。

a) JVM Class loader:位于整个层次结构的最上层,它是整个类加载器层次结构的根,因此它没有父类加载器。这个类加载器负责加载JVM类, JVM 扩展类,以及定义在classpath 环境变量上的所有的Java类。

b) WebSphere Extensions Class loader:WebSphere 扩展类加载器, 它将加载WebSphere的一些runtime 类,资源适配器类等。

c) WebSphere lib/app Class loader:WebSphere服务器类加载器,它将加载WebSphere安装目录下$(WAS_HOME)/lib/app路径上的类。在WAS v4版本中,WAS使用这个路径在所有的应用程序之间共享jar包。从WAS v5开始, 共享库功能提供了一种更好的方式,因此,这个类加载器主要用于一些原有的系统的兼容。

d) WebSphere "server" Class loader:WebSphere应用服务器类加载器。它定义在这个服务器上的所有的应用程序之间共享的类。WAS v5中有了共享库的概念之后,可以为应用服务器定义多个与共享库相关联的类加载器,他们按照定义的先后顺序形成父子关系。

e) Application Module Class Loader:应用程序类加载器,位于层次结构的最后一层,用于加载J2EE应用程序。根据应用程序的类加载策略的不同,还可以为Web模块定义自己的类加载器。

关于WebSphere的类加载器的层次结构,以下的几点说明可能更有助于进一步的理解类的查找和加载过程:

a) 每个类加载器负责在自身定义的类路径上进行查找和加载类。

b) 一个子类加载器能够委托它的父类加载器查找和加载类,一个加载类的请求会从子类加载器发送到父类加载器,但是从来不会从父类加载器发送到子类加载器。

c) 一旦一个类被成功加载,JVM 会缓存这个类直至其生命周期结束,并把它和相应的类加载器关联在一起,这意味着不同的类加载器(平级或上下级之间)可以加载相同名字的类。

d) 如果一个加载的类依赖于另一个或一些类,那么这些被依赖的类必须存在于这个类的类加载器查找路径上,或者父类加载器查找路径上。

如果一个类加载器以及它所有的父类加载器都无法找到所需的类,系统就会抛出ClassNotFoundExecption异常或者NoClassDefFoundError的错误。

JVM GC垃圾回收和ClassLoader类加载器之间的微妙关系 如果您的应用中存在以下类似代码:

private void x1(){  
    for(;;){  
        List c = new ArrayList();  
    }  
}
private void x1() {  
    for (;;) {  
        List c = new ArrayList();  
    }  
}

这样代码运行时会不断重复地申请ArrayList新对象内存空间,但是此代码并不会导致内存泄漏OutOfMemory的现象。因为不断申请的 ArrayList新对象会被立即丢弃成为垃圾对象,最终在JVM GC过程中回收,并释放出所占用的Heap内存空间,从而我们可以不断地申请到新对象所需的内存空间。

现在我们以Servlet为例,演示下面代码正常运行情况下在内存中的使用情况

public class Servlet1 extends HttpServlet{  
    private static final String STATICNAME="Simple";  
    protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException{  
    }  
}
public class Servlet1 extends HttpServlet {  
    private static final String STATICNAME = "Simple";  
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {  
    }  
}

当此Serlvet1类被加载到内存中运行时,以下对象objects和类Class将会在内存中存在,互相关联关系图示如下(核心的几个类和对象):

图中黄色方框标识的是由应用ClassLoader类加载器(AppClassLoader)加载的类和对应的类实例化对象,其他以绿色方框进行标 识。其中图示化(简化)的J2EE容器对象Container引用指向了为此J2EE应用所创建的应用类加载器AppClassLoader对象,与此同 时引用还指向了由应用类加载器AppClassLoader所加载创建的Serlvet1实例对象。当有外界HTTP请求此Servlet1时,容器 Container将会调用Servlet1实例对象的doGet()方法来提供服务。

其中几点应该引起注意:

a) 其中STATICNAME是被Class Servlet1类本身所有,并不属于Servlet1类实例对象。

b) 其中Servlet1实例对象含有引用指向Servlet1.class类本身。

c) 每一个Class类含有一个引用指向加载此Class类的类加载器AppClassLoader对象。

d) 同时每一个类加载器AppClassLoader对象含有一个引用指向由其加载的类Class

从上面的图示中我们可以清晰的得知,如果AppClassLoader以外的类加载器所加载的对象引用了任何一个由AppClassLoader加载的对象,那么由AppClassLoader加载的任何Class(包括AppClassLoader本身)将不能被垃圾回收。此结论非常重要,这是出现上面我们描述Class内存泄漏现象最根本的原因,后面我们会阐述此现象是如何被触发的。

正常情况下,如果上面部署的应用被卸载或被停止,那么Container对象将会与应用相关的任何类和对象(如Servlet1实例对象、 AppClassLoader类加载器实例)断开引用关联关系,从而这些与被停止应用相关的所有类和类实例将会被JVM进行抛弃成为垃圾并进行内存回收。

正如上图所示,Servlet1应用相关的类、对象、类加载器对象等等所有的一切都和根线程对象没有任何的关联,从而最终会被JVM进行垃圾回收。

现在我们来演示一个非正常情况下的样例,正是此“非正常”导致应用的所有Class类不能从内存中正确销毁。此处我们在原来的样例Servlet1中引入一个特殊的Class类和其实例:Level,改写样例代码如下:

package com.test;  
import java.io.*;  
import java.util.logging.*;  
import javax.servlet.*;  
import javax.servlet.http.*;  
  
public class LeakServlet extends HttpServlet{  
    private static final String STATICNAME = "Thisleaks!";  
    private static final Level CUSTOMLEVEL = new Level("test",550){};//anonclass!  
    protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException{  
        Logger.getLogger("test").log(CUSTOMLEVEL,"doGetcalled");  
    }  
}
package com.test;  
import java.io.*;  
import java.util.logging.*;  
import javax.servlet.*;  
import javax.servlet.http.*;  
public class LeakServlet extends HttpServlet {  
    private static final String STATICNAME = "This leaks!";  
    private static final Level CUSTOMLEVEL = new Level("test", 550) {}; // anon class!    
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {  
        Logger.getLogger("test").log(CUSTOMLEVEL, "doGet called");  
    }  
}

注意其中CUSTOMLEVEL是一个匿名类型的类实例对象,因为Level类的构造方法属性的protected,不能被直接构造,所以我们必须创建新的类new Level("test", 550) {},从而形成LeakServlet的内置类,最终编译会生成LeakServlet$1.class

当此LeakServlet.class类被加载到内存中运行时,以下对象objects和类Class将会在内存中存在,互相关联图示如下(核心的几个类和对象):

在这张图示中出现了你意想不到的景象:系统Level类实例使用了一个类静态变量 ArrayList来保存所有创建的所有类型Level的实例对象,我们可以通过JDK Level类源代码来进行验证:

public class Level implements java.io.Serializable{  
    private static java.util.ArrayList known = new java.util.ArrayList();  
    ……  
    protected Level(String name,int value){  
        this(name,value,null);  
    }  
    protected Level(String name,int value,String resourceBundleName){  
        if(name==null){  
        throw new NullPointerException();  
        }  
        this.name=name;  
        this.value=value;  
        this.resourceBundleName=resourceBundleName;  
        synchronized(Level.class){known.add(this);}  
    }  
    ……  
}
public class Level implements java.io.Serializable {  
    private static java.util.ArrayList known = new java.util.ArrayList();  
    ……  
    protected Level(String name, int value) {  
        this(name, value, null);  
    }  
    protected Level(String name, int value, String resourceBundleName) {  
        if (name == null) {  
          throw new NullPointerException();    
        }  
        this.name = name;  
        this.value = value;  
        this.resourceBundleName = resourceBundleName;  
        synchronized (Level.class) { known.add(this); }  
    }  
    ……  
}

当此应用被卸载或停止时,那么JVM GC能做那些事情呢?

严重的事情发生了,在所有类和实例对象中仅仅是LeakServlet实例对象才能被JVM GC回收,其他的任何由AppClassLoader加载的类都无法被JVM GC从内存中销毁删除。

因为Level类属于WebSphere应用服务器JRE核心系统类,存在于JRE核心类库core.jar文件中,是由JVM ClassLoader最上层类加载器来进行加载的,也就是Level.class类并不隶属于应用的类加载器AppClassLoader。

当应用被卸载或停止时,Level.class是由系统JVM加载的,所以Level.class是不会被回收的,那么它引用的 CUSTOMLEVEL实例变量和其对应的类LeakServlet$1将不会被回收,从而导致AppClassLoader对象无法被JVM回收,从而 最终导致此应用中的所有Class类(Class MetaData属性描述)无法被JVM当作垃圾来进行内存回收。

当应用重新启动时,将由新创建的AppClassLoader来重新加载所有的Class类,那么此时内存中就存在了同一Class类的多分拷贝。

这样就形成了臭名昭著的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

public abstract class LogFactory{  
    protected static Hashtable factories=null;  
    ……  
    publics tatic Log getLog(Class clazz) throws LogConfigurationException{  
        returngetFactory().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 cache Factory(ClassLoader classLoader,LogFactory factory){  
    if(factory!=null)  
    if(classLoader==null)  
        nullClassLoaderFactory=factory;  
    else  
        factories.put(classLoader,factory);  
    }  
    ……  
}
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

package java.beans;  
……  
public class Introspector{  
    ……  
    private static Map declaredMethodCache=Collections.synchronizedMap(newWeakHashMap());  
    private static Map beanInfoCache=Collections.synchronizedMap(newWeakHashMap());  
    ……  
    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);  
    }  
    returnbi;  
    }  
    ……  
}
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以后的版本)

package org.springframework.beans;  
......  
final class CachedIntrospectionResults{  
    private static final Log logger;  
    private static final Map classCache=Collections.synchronizedMap(newWeakHashMap());  
    private final BeanInfo beanInfo;  
    private final Map propertyDescriptorCache;  
    static{  
        logger=LogFactory.getLog(org.springframework.beans.CachedIntrospectionResults.class);  
    }  
    public static CachedIntrospectionResultsforClass(ClassbeanClass) 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("Usingcachedintrospectionresultsforclass["+beanClass.getName()+"]");  
        return  results;  
    }  
  
    private CachedIntrospectionResults(Class clazz) throws BeansException{  
        ……  
        beanInfo=Introspector.getBeanInfo(clazz);  
        Class classToFlush=clazz;  
        ……  
    }  
    ……  
}
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对应代码,可以非常直观地定位问题:

package org.apache.commons.beanutils;  
……  
public class MethodUtils{  
privatestaticWeakHashMapcache=newWeakHashMap();  
    ……  
    public static Method getAccessibleMethod(Class clazz,String methodName,Class parameterTypes[])  
{  
        Method Descriptormd;  
        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;  
        }  
        ……  
    }  
}
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

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(intop){  
        this.operation=op;  
    }  
    public boolean isNewOperation(){  
        return (this.operation==2)||  
        (this.operation==4)||  
        (this.operation==192);  
    }  
}
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类的样例代码

public class LeakCauseServletInOtherApp extends HttpServlet{  
    private final OperationEnum operation=OperationEnum.CHANGE;//  
    protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException{  
        doSomething(request,response);  
    }  
    ……  
}
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以后的版本才存在)

<?xmlversion="1.0"encoding="UTF-8"?>
<web-appid="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/j2eehttp://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>
……
<?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以后版本的代码)

package org.springframework.web.util;  
……  
public class IntrospectorCleanupListener implements ServletContextListener{  
    public void contextInitialized(ServletContextEvent event){  
        CachedIntrospection Results.acceptClassLoader(Thread.currentThread().getContextClassLoader());  
    }  
    public void contextDestroyed(ServletContextEvent event){  
        CachedIntrospectionResults.clearClassLoader(Thread.currentThread().getContextClassLoader());  
        Introspector.flushCaches();  
    }  
}
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

packagecom.test;  
import javax.servlet.ServletContextEvent;  
import javax.servlet.ServletContextListener;  
import org.apache.commons.logging.LogFactory;  
public class ApplicationLifecycleListener implements ServletContextListener{  
    publicvoidcontextDestroyed(final ServletContextEvent sce){  
    LogFactory.release(Thread.currentThread().getContextClassLoader());  
}  
public void contextInitialized(final ServletContextEvent sce){  
}  
}
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配置文件

<?xmlversion="1.0"encoding="UTF-8"?>
<web-appid="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/j2eehttp://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类加载器内存泄漏问题。!

# 精彩推荐 #

Spring Boot 快速入门系列(II)—— 数据操作篇之 Spring Data JPA

Spring Boot 快速入门系列(III)—— 数据操作篇之 JdbcTemplate

Spring Boot 快速入门系列(IV)—— 数据操作篇之 MyBatis

Spring Boot 快速入门系列(V)—— 事务管理篇之 @Transactional

Spring Boot 快速入门系列(VI)—— 接口规范篇

转自:https://www.iteye.com/blog/ligangty-1666103
参考资料:
  • 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

原文发布于微信公众号 - AiSmart4J(smart4j)

原文发表时间:2019-09-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券