关于websphere在控制台中进行重启应用,而不是重启整个websphere,这时候静态类是不能回收的,造成些类不能销毁,占用着内存,而 且这些内存是不能重复使用的,可以说是内存泄露。静态的类不能销毁,那么静态类引用的对象也不能销毁,因此一些bean都不能被正常回收,其实这些小对象 占用内存是很少的,最主要的是这些类引用的缓存没有销毁,这些缓存才是占用内存的大头。如果系统用了一两天,然后有人在控制台上将这个应用重启,那么缓存 将不能销毁,照成大量内存浪费,因此现在我们分析的dump文件中缓存一半的内存是由这些没有销毁的无用的缓存占用的。其实BSP中有个 HttpServletContextListener,这个监听器能够在关闭应用的时候清空缓存,但是从dump文件中可以看出这个监听器可能没有在应 用关闭的时候调用。
对于静态类支持有的对象销毁问题需要进行研究,解决Websphere的这种bug。
”
所以大家认为是WAS的Bug导致:在进行应用重新启动过程中,一些类的Class MetatData本身描述不能从内存中被释放,同时在LouShang烟草系统中一些系统类型的类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" );
- }
- }
- }
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"); } } }
相信在LouShang烟草系统中这种静态类的应用场景并不多见,所以上面提到的静态类的说法并不准确,应该改正为:类静态变量。
虽然把“静态类”改变为“类静态变量”,但是上面提到的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.iteye.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 >
<!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 = new byte [ 2024000 ];
- public ClassLoadBugServlet() { 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 = new StaticClass();
- response.getOutputStream().print(sc.test());
- System.out.println(sc.test());
- }
- }
public class ClassLoadBugServlet extends javax.servlet.http.HttpServlet implements javax.servlet.Servlet { private static byte[] testByteArray = new byte[2024000]; public ClassLoadBugServlet() {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 = new StaticClass(); response.getOutputStream().print(sc.test()); System.out.println(sc.test()); } }
StaticClass.java
- public class StaticClass {
- private static byte [] testByteArray = new byte [ 2024000 ];
- public String test(){ return "Test Class Loader" ; }
- }
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 重复加载问题得以再现
根据浪潮软件浪潮烟草v3和LouShang v3系统核心框架构建在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) 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;
- }
- }
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
- <? 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 >
<?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 = "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" );
- }
- }
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类加载内存泄漏问题。