1 概述 2
1.1 背景 2
2 Java内存泄漏 2
2.1 Java是如何管理内存 2
2.2 什么是Java的内存泄漏 2
2.3 如何跟踪Java内存泄漏 3
3 调查内容 4
3.1 环境配置 4
3.1.1 安装JRockit JDK 1.4 4
3.1.2 修改Eclipse默认JRE为JRockit 5
3.1.3 修改部分源码使其适应JDK1.4标准 5
3.1.4 修改JVM才参数,开放JMX调试服务 6
3.1.5 配置BEA JRockit(R) Mission Control连接 6
3.1.6 跟踪资源使用情况 7
3.2 PC端功能 7
3.2.1 ThreadLocal缓存对象生命周期跟踪 7
3.2.1.1 修改方式 9
3.2.2 Session对象回收周期过长 9
3.2.2.1 修改方式 9
4 参考资料 10
X项目中系统使用过程中,运行时间过长出现内存资源持续占用不能被及时回收的情况,并且其资源占用量随运行时间与并发性成正相关,初步判断为Java内存泄漏导致,所以进行内存泄漏方面的调查分析。
Java通过GC功能不定时地回收内存资源,GC判断一个对象是否可以回收的标准是该对象是否可达,即是否被引用,JVM在以有向图的方式管理所有堆上资源,这使环形循环引用的不可达对象依旧可以被有效的回收,下图简单的描述了一个可回收单元的产生。
熟悉C/C++语言的开发人员都知道,C/C++语言的内存泄漏指的是不可回收资源,是纯粹的内存黑洞,除非进程终止后操作系统回收进程所有堆上资源时才会被释放,这是纯粹的内存泄漏;而Java语言的内存泄漏与C/C++语言所指的内存泄漏非同一概念,Java语言所指的内存泄漏是指全局/静态集合持续被持有而导致的无用单元不可回收,是一种逻辑上的“内存泄漏”。
很多应用服务器厂商都提供了功能强大的GC跟踪工具,用于分析堆上内存分配与收集情况,从而确定内存泄漏位置。本次调查采用的是BEA公司发布的“BEA JRockit(R) Mission Control 2.0”。开发人员试用版本许可可以使用1个小时。
使用BEA JRockit(R) Mission Control 2.0所包含的如下三个工具辅助内存泄漏分析:
BEA JRockit Management Console
BEA JRockit Runtime Analyzer (JRA)
BEA Memory Leak Detector
具体使用方法,参考BEA官方网站说明。
BEA官方网站下载开发人员试用版本的JRocket JDK工具包jrockit-R27.2.0-jdk1.4.2_13,安装后导入试用版本许可文件,使用版本可以免费使用1个小时,重新启动可以继续使用。
导入试用许可证:
<C:\Program Files\Java\jrockit-R27.2.0-jdk1.4.2_13\jre>
<?xml version="1.0" encoding="ISO-8859-1"?> <bea-licenses> <license-group format="1.0" product="JRockit" release="*"> <license component="JRA" cpus="unvalued" expiration="never" hours="1" ip="any" licensee="BEA Evaluation Customer" serial="454493271161-2222564495787" type="SDK" signature="MC0CFFCnVsUSP96Xd5YtNPTQKXcGoiBgAhUA4yH2/UmqYpJLNzPOPmJUdqPUrNs=" /> <license component="Memory Leak Detector" cpus="unvalued" expiration="never" hours="1" ip="any" licensee="BEA Evaluation Customer" serial="454493271161-2222564495787" type="SDK" signature="MCwCFFVySk0b0fwMReywZt4UyBZX7PjEAhQibfVzVVDZgkXgnMxZUfFzAS5gew==" /> </license-group> </bea-licenses>
LoginUser用于提高系统性能的缓存对象,缓存内容包括,用户信息,商品信息,分页信息等。按照设计者的初衷,其生命周期的可控制范围为Session, 当用户注销或Session超时后会被移出,内存资源会被回收。
但是,在LoginUser中引入了ThreadLocal的静态成员,用于缓存当前登录用户的LoginUser对象,其生命周期与当前线程绑定,请求完成后并没有从ThreadLocal中销毁,而滞留于静态存贮区域,当用户注销后,该对象仍然无法被销毁,从而使LoginUser对象生命周期失控,直至该线程再次被分配时才会被新的对象覆盖,使其区域不可达。当同一个用户的请求被多次提交时该对象的引用将会逐渐的产生更多的引用,从而更难于销毁。多用户登录注销后,会导致更多的无效内存无法回收。
跟踪过程:
对象引用关系(请放大观看)
LoginUser对象被两个集合引用,分别为Session和ThreadLocal。经过若干页面操作后,ThreadLocal集合中的引用会逐渐增多,从而使对象生命周期更加难于控制。
如上图所示,LoginUser被从Session中清除,但是ThreadLocal中的引用一直存在,并且其引用数目不可控制,可能会很多,引用数目越多其生命命周期可能会更长。从而导致内存泄漏。
将LoginUser对象与Resource对象的ThreadLocal引用周期控制在Servlet请求周期之内,但此请求完成后,释放其对对象的引用,使其始终保持Session的单一引用,即可严格控制集合对象的生命周期。
Servlet
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { License license = new License(); HtmlPage page = null; ......... ......... ......... } // add for clear memory leak /begin finally { LoginUser.setCurrentLoginUser(null); ResourceManager.setBundle(null); } // add for clear memory leak /end }
相同的用户多次登录系统而不注销的情况下,先登录的用户的Session以及LoginUser对象会多次被创建,而不能及时的被清除,这给峰值操作时的资源占用带来了隐患,由于用户基本上没有注销的习惯(通常情况下,用户都是直接关闭浏览器的),Session的超时时间通常是30分钟,那么这部分对象将在30分钟后才能被回收。
由于LoginUser中的对象可能会很大,由此所带来的内存消耗是非常大的,又因为同一个用户多次登录,LoginUser没有必要重复创建。
1. 将系统主窗口中增加关闭事件,关闭时产生注销操作。
a) 优点:
i. 可以缩短Session中的LoginUser无用生命周期,使资源第一时间被回收。
ii. 用户使用更加安全,不会因为忘记注销而产生安全漏洞。
b) 缺点:
i. 如果用户习惯了关闭主窗口然后使用子窗口进行操作,将会导致Session无效,比如:登陆后用户进入报告登录,然后关闭驻窗口进行报告登录等。
ii. 浏览器关闭时间不可靠,可能不会被调用。
2. 重复用户登录处理Session的销毁
a) 原则上讲,Serviet不允许当前Session销毁其他的Session,但是有一个解决办法,可以使用接口HttpSessionListener编写自己的侦听器,接口方法如下:
void sessionCreated(HttpSessionEvent se) void sessionDestroyed(HttpSessionEvent se) 通过HttpSessionEvent可以获取每个创建的session信息 web.xml中定义 <listener> <listener-class>loginuser.ContextListener</listener-class> </listener>
http://www-128.ibm.com/developerworks/cn/java/l-JavaMemoryLeak/
http://dev2dev.bea.com/blog/sla/archive/2005/06/jrockit_memory.html
http://commerce.bea.com/products/weblogicjrockit/1.4.2/142_x.jsp?
http://commerce.bea.com/downloadproduct.jsp?family=JRMC&major=2.0&minor=0&delivery=1&os=All&intent=purchase
http://java.sun.com/j2se/1.4.2/docs/api/java/lang/ThreadLocal.html