wabacus框架在Myeclipse reload过程中方法区溢出问题讨论
2016-06-20 17:10
507 查看
问题解析
总结
问题解析
(下面的文章都是基于个人的知识,由于本人是个菜鸟,欢迎指正)
现在手上有一个比较简单的信息管理系统的小项目,最开始立项的时候我还没有来到学校,已经定下来采用wabacus框架去做,至于wabacus框架是个什么东西,详情请点击。
里面提到的不用或者很少,是建立在你有强大的sql编写能力的基础上。整个框架是采用xml文件配置的方式,配置一些例如报表的列及其属性,数据来源,js,css等,通过框架解析成web的前后台形式。
最近在开发的时候,jvm的方法区大小没有手动去调整,默认80m。经过几次改动并reload时,在方法区发生了oom。目前为止,该错误只出现了开发中redeploy时,在正常的项目测试中tomcat关闭时都会出现这个提示。经过查看tomcat给出的日志,发现在redeploy时有提示两个线程未正常退出,可能会造成内存泄漏。
首先要找到这两个线程因为什么没有退出。
通过jps命令找到这两个线程坐在的进程id,在通过jstack命令查看该进程中的所有线程状态。如下图
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/855fc83a93c7e75c36b93a5cd8005706)
下图是Thread-1的详情,在使用jstack命令查看线程时发现,thread-2线程开始处于sleep状态,一段时间后退出,后续辉仔框架源码解析中给出解释。先看Thread-1.Thread-1,根据jsatck命令查看到的信息,可以初步的判断,该进程出入waiting状态无法退出,更具提示信息发现是wabacus框架提供的一个关于文件上传的进程一直出入等待状态无法退出,根据提示,去查看wabacus源码中具体是怎么实现的。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/f223719911f8837d943e2603a65be167)
关于wabacus的启动,这里做一下简单的介绍,在我浅显的理解基础上!在web.xml中能看到下面代码,顺藤摸瓜,找到该listener源码。
com.wabacus.WabacusServlet 源码
分析上面主要代码,上来首先在加载xml的时候就关闭了数据源closeAllDatasources(),应该是在实现框架热部署时,如果修改了xml里面数据源的配置,首先要关闭数据源,重新加载。之后又做了一些路径上的处理。
最后三个方法比较重要。
本文章所要解决的问题就在最后两个线程上,关于报表的加载辉仔后续文章中剖析。在看一下销毁方法。
问题就出在了listener销毁时,两个线程没有正常退出。下面对第一个线程源码做剖析。
AbsDataImportThread没多少东西就是一个继承了Thread的很简单的抽象类
FileUpDataImportThread 源码
单例设计模式,采用的是饿汉式单例,天生安全。
上面是run和stopRunning方法,通过RUNNING_FLAG标志安全退出线程,看似没问题,那就是
框架提供了一个可以通过上传excel文件完成批量数据的导入,
当queueInstance队列为空时,线程wait等待用户上传文件,一旦用户上传文件,跳出while,将文件添加到了一个新的队列中返回给FileUpDataImportThread,完成数据的导入,这里貌似没有问题。但是在,FileUpDataImportThread线程退出时,
对while循环添加一个flag,退出的时候改变flag状态,个人认为凡是在线程中出现了不断检查状态的while循环一般都需要加上一个flag作为退出标志。
接下来分析Thread-2线程,也就是 TimingThread,在使用jstack命令查看的时候,发现改线程处于sleep状态。
通过查看源码,得知这个线程在用户指定时间内运行一个指定的task,在reload或者项目关闭时,该线程仍然处于sleep状态无法退出,这俩在线程退出方法中interrupt该线程,使该线程退出sleep,正常退出。
通过上面的分析,把修改后的框架编译之后和之前的框架做一下对比,在reload和tomcat关闭时。下面是对比详情。工具jvisualvm。
原框架,项目运行起来后,PermGen使用情况
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/43e985e2dddf982df704d68e4a5a584d)
reload 2次后PermGen使用情况
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/1086cb4c333a20624dd09721dc0be593)
类装载和卸载情况
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/95235ab528a0ad07c783347eb4c139ba)
reload 4次后PermGen使用情况
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/12749488a88c3d0b3c17ec8fab5529bc)
类装载和卸载情况
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/e4b43df06fec5e1278078f1361f4f2f5)
此时在框架加载框架所需要的类文件时出现PermGen的oom。
原框架,项目运行起来后,PermGen使用情况
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/3d07bb7d0f528167e5b83a2bb31b112b)
reload 2次后PermGen使用情况
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/d2e692b40ce186bf00c9e6c0014161d1)
类装载和卸载情况
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/45826c576f42017577ac3ce168a7d6d1)
reload 4次后PermGen使用情况
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/5fd90d7136a939c722571781269071c9)
类装载和卸载情况
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/a1931599dc12d4fba3378b497e666594)
第四次reload后,出现了PermGen的垃圾回收,大量类被卸载,从堆的dump文件上也可以看出,原框架大量类没有被卸载。下面是对比图
原框架。reload2次后
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/150e3b03f86d4ca26ebe57a9d20db499)
修改后,reload4次后
![](https://oscdn.geek-share.com/Uploads/Images/Content/202012/08/e43c454f5f7c19e461e41fc086e51a43)
总结
在reload和tomcat关闭时,内存泄漏的提示也没有了,从之前的提示上来看,个人感觉是线程为正常退出,导致classloader不能够被卸载,从而导致classloader加载的类不能被卸载,导致方法区内存溢出。
对于本文讨论的两个线程,感觉处于sleep状态的线程,在一段时间后会自动退出,影响没有那么大,而waiting状态的进程,是不可能退出的,在reload时会导致内存的泄漏。而在tomcat关闭后能够顺利退出,因为两个进程都是daemon进程,在程序退出后,线程也就退出了。但是,在这里,两个线程应不应该是daemon线程,待后续深入两个线程到底干了什么后在讨论。
总结
问题解析
(下面的文章都是基于个人的知识,由于本人是个菜鸟,欢迎指正)
现在手上有一个比较简单的信息管理系统的小项目,最开始立项的时候我还没有来到学校,已经定下来采用wabacus框架去做,至于wabacus框架是个什么东西,详情请点击。
Wabacus框架,是一个能大大提高J2EE项目开发效率的通用快速开发框架,与ExtJs,JQuery等纯客户端框架不同, 它提供的是前后台的完整解决方案,可以完成SSH框架的功能,但是开发效率比它快好几倍,因为基本上不用编写JSP/JAVA代码,或只要编写很少的代码。 ----摘自wabacus官网
里面提到的不用或者很少,是建立在你有强大的sql编写能力的基础上。整个框架是采用xml文件配置的方式,配置一些例如报表的列及其属性,数据来源,js,css等,通过框架解析成web的前后台形式。
最近在开发的时候,jvm的方法区大小没有手动去调整,默认80m。经过几次改动并reload时,在方法区发生了oom。目前为止,该错误只出现了开发中redeploy时,在正常的项目测试中tomcat关闭时都会出现这个提示。经过查看tomcat给出的日志,发现在redeploy时有提示两个线程未正常退出,可能会造成内存泄漏。
六月 04, 2016 9:56:59 上午 org.apache.catalina.loader.WebappClassLoader clearReferencesThreads 严重: The web application [/MySystem] appears to have started a thread named [Thread-1] but has failed to stop it. This is very likely to create a memory leak. 六月 04, 2016 9:56:59 上午 org.apache.catalina.loader.WebappClassLoader clearReferencesThreads 严重: The web application [/MySystem] appears to have started a thread named [Thread-2] but has failed to stop it. This is very likely to create a memory leak.
首先要找到这两个线程因为什么没有退出。
通过jps命令找到这两个线程坐在的进程id,在通过jstack命令查看该进程中的所有线程状态。如下图
下图是Thread-1的详情,在使用jstack命令查看线程时发现,thread-2线程开始处于sleep状态,一段时间后退出,后续辉仔框架源码解析中给出解释。先看Thread-1.Thread-1,根据jsatck命令查看到的信息,可以初步的判断,该进程出入waiting状态无法退出,更具提示信息发现是wabacus框架提供的一个关于文件上传的进程一直出入等待状态无法退出,根据提示,去查看wabacus源码中具体是怎么实现的。
关于wabacus的启动,这里做一下简单的介绍,在我浅显的理解基础上!在web.xml中能看到下面代码,顺藤摸瓜,找到该listener源码。
<listener> <listener-class>com.wabacus.WabacusServlet</listener-class> </listener>
com.wabacus.WabacusServlet 源码
public class WabacusServlet extends HttpServlet implements ServletContextListener//既是一个Listener又是一个Servlet的,Servlet的主要作用,估计是为了框架实现热部署,这点有待后续验证·····
public void contextInitialized(ServletContextEvent event) { closeAllDatasources(); Config.homeAbsPath=event.getServletContext().getRealPath("/"); Config.homeAbsPath=FilePathAssistant.getInstance().standardFilePath(Config.homeAbsPath+"\\"); /*try { Config.webroot=event.getServletContext().getContextPath(); if(!Config.webroot.endsWith("/")) Config.webroot+="/"; }catch(NoSuchMethodError e) { Config.webroot=null; }*/ Config.webroot=null; Config.configpath=event.getServletContext().getInitParameter("configpath"); if(Config.configpath==null||Config.configpath.trim().equals("")) { log.info("没有配置存放配置文件的根路径,将使用路径:"+Config.homeAbsPath+"做为配置文件的根路径"); Config.configpath=Config.homeAbsPath; }else { Config.configpath=WabacusAssistant.getInstance().parseConfigPathToRealPath( Config.configpath,Config.homeAbsPath); } loadReportConfigFiles(); FileUpDataImportThread.getInstance().start(); TimingThread.getInstance().start(); }
分析上面主要代码,上来首先在加载xml的时候就关闭了数据源closeAllDatasources(),应该是在实现框架热部署时,如果修改了xml里面数据源的配置,首先要关闭数据源,重新加载。之后又做了一些路径上的处理。
最后三个方法比较重要。
//加载所有报表配置文件 loadReportConfigFiles(); //开启一个线程,初步判断是用来处理文件上传的,后续后深入剖析。 FileUpDataImportThread.getInstance().start(); //开启定时线程,初步判断是用来一定时间内完成对数据的备份 TimingThread.getInstance().start();
本文章所要解决的问题就在最后两个线程上,关于报表的加载辉仔后续文章中剖析。在看一下销毁方法。
public void contextDestroyed(ServletContextEvent event) { //关闭数据源 closeAllDatasources(); //停止线程 FileUpDataImportThread.getInstance().stopRunning(); //停止线程 TimingThread.getInstance().stopRunning(); } private void closeAllDatasources() { Map<String,AbsDataSource> mDataSourcesTmp=Config.getInstance().getMDataSources(); if(mDataSourcesTmp!=null) { for(Entry<String,AbsDataSource> entry:mDataSourcesTmp.entrySet()) { if(entry.getValue()!=null) entry.getValue().closePool(); } } }
问题就出在了listener销毁时,两个线程没有正常退出。下面对第一个线程源码做剖析。
public class FileUpDataImportThread extends AbsDataImportThread
AbsDataImportThread没多少东西就是一个继承了Thread的很简单的抽象类
public abstract class AbsDataImportThread extends Thread { protected boolean RUNNING_FLAG=true; public void restart() { RUNNING_FLAG=true; start(); } public void stopRunning() { RUNNING_FLAG=false; } }
FileUpDataImportThread 源码
private final static FileUpDataImportThread instance=new FileUpDataImportThread(); private FileUpDataImportThread() {} public static FileUpDataImportThread getInstance() { return instance; }
单例设计模式,采用的是饿汉式单例,天生安全。
public void run() { while(RUNNING_FLAG) { try { List<Map<List<DataImportItem>,Map<File,FileItem>>> lstUploadFiles=UploadFilesQueue .getInstance().getLstAllUploadFiles(); log.debug("上传文件线程启动,正在进行文件上传........................."); for(Map<List<DataImportItem>,Map<File,FileItem>> mUploadFilesTmp:lstUploadFiles) { if(mUploadFilesTmp.size()==0) continue; Entry<List<DataImportItem>,Map<File,FileItem>> entry=mUploadFilesTmp.entrySet().iterator().next(); doDataImport(entry.getKey(),entry.getValue()); } }catch(Exception e) { log.error("数据导入线程运行失败",e); } } } public void stopRunning() { super.stopRunning(); UploadFilesQueue.getInstance().notifyAllThread(); }
上面是run和stopRunning方法,通过RUNNING_FLAG标志安全退出线程,看似没问题,那就是
UploadFilesQueue.getInstance().notifyAllThread();出问题了。
框架提供了一个可以通过上传excel文件完成批量数据的导入,
lstUploadFiles=UploadFilesQueue .getInstance().getLstAllUploadFiles();是获取上传文件队列中的文件,然后调用doDataImport方法完成数据的导入,这里不做过多解释,主要看一下线程问题。UploadFilesQueue采用的也是单例模式,在getLstAllUploadFiles()函数里面,出现问题了,看源码。
public List<Map<List<DataImportItem>,Map<File,FileItem>>> getLstAllUploadFiles() { synchronized(queueInstance) { while(queueInstance.size()==0) { try { queueInstance.wait(); }catch(InterruptedException e) { e.printStackTrace(); } } List<Map<List<DataImportItem>,Map<File,FileItem>>> lstResults=new ArrayList<Map<List<DataImportItem>,Map<File,FileItem>>>(); lstResults.addAll(queueInstance); queueInstance.clear(); return lstResults; } } public void notifyAllThread() { synchronized(queueInstance) { queueInstance.notifyAll(); } }
当queueInstance队列为空时,线程wait等待用户上传文件,一旦用户上传文件,跳出while,将文件添加到了一个新的队列中返回给FileUpDataImportThread,完成数据的导入,这里貌似没有问题。但是在,FileUpDataImportThread线程退出时,
UploadFilesQueue.getInstance().notifyAllThread();通知正在等待UploadFilesQueue状态的线程退出waiting状态,但是即使退出waiting状态又如何呢,还是没有跳出while循环,接下来在UploadFilesQueue是空的情况下,FileUpDataImportThread任然还是出入waiting中,稍加改动,如下。
public List<Map<List<DataImportItem>,Map<File,FileItem>>> getLstAllUploadFiles() { synchronized(queueInstance) { **while(Flag&&queueInstance.size()==0)** { System.out.println("我还没有退出!"); try { queueInstance.wait(); }catch(InterruptedException e) { e.printStackTrace(); } System.out.println("我wait结束了!"); } System.out.println("我跳出了while循环!size:"+queueInstance.size()); List<Map<List<DataImportItem>,Map<File,FileItem>>> lstResults=new ArrayList<Map<List<DataImportItem>,Map<File,FileItem>>>(); lstResults.addAll(queueInstance); queueInstance.clear(); return lstResults; } } public void notifyAllThread() { synchronized(queueInstance) { **Flag = false;** queueInstance.notifyAll(); } }
对while循环添加一个flag,退出的时候改变flag状态,个人认为凡是在线程中出现了不断检查状态的while循环一般都需要加上一个flag作为退出标志。
接下来分析Thread-2线程,也就是 TimingThread,在使用jstack命令查看的时候,发现改线程处于sleep状态。
public void run() { while(RUNNING_FLAG) { if(this.lstTasks==null||this.lstTasks.size()==0) break; for(ITask taskObjTmp:lstTasks) { try { if(taskObjTmp.shouldExecute()) taskObjTmp.execute(); }catch(Exception e) { log.error("执行任务:"+taskObjTmp.getTaskId()+"时失败",e); } } if(this.intervalMilSeconds==Long.MIN_VALUE) { intervalMilSeconds=Config.getInstance().getSystemConfigValue("timing-thread-interval",15)*1000L; if(intervalMilSeconds<=0) intervalMilSeconds=15*1000L; } if(this.intervalMilSeconds>0) { da8f try { Thread.sleep(this.intervalMilSeconds); }catch(InterruptedException e) { log.warn("wabacus定时运行线程被中断睡眠状态",e); } } } }
通过查看源码,得知这个线程在用户指定时间内运行一个指定的task,在reload或者项目关闭时,该线程仍然处于sleep状态无法退出,这俩在线程退出方法中interrupt该线程,使该线程退出sleep,正常退出。
public void stopRunning() { RUNNING_FLAG=false; interrupt(); if(this.lstTasks!=null) { for(ITask taskObjTmp:lstTasks) { try { taskObjTmp.destory(); }catch(Exception e) { log.error("停止任务:"+taskObjTmp.getTaskId()+"时失败",e); } } } }
通过上面的分析,把修改后的框架编译之后和之前的框架做一下对比,在reload和tomcat关闭时。下面是对比详情。工具jvisualvm。
原框架,项目运行起来后,PermGen使用情况
reload 2次后PermGen使用情况
类装载和卸载情况
reload 4次后PermGen使用情况
类装载和卸载情况
此时在框架加载框架所需要的类文件时出现PermGen的oom。
java.lang.OutOfMemoryError: PermGen space at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:800) at java.lang.ClassLoader.defineClass(ClassLoader.java:643) at com.wabacus.util.WabacusClassLoader.loadClass(WabacusClassLoader.java:110) at com.wabacus.system.assistant.ReportAssistant.buildPOJOClass(ReportAssistant.java:461) at com.wabacus.system.assistant.ReportAssistant.buildReportPOJOClass(ReportAssistant.java:407) at com.wabacus.config.component.application.report.ReportBean.loadPojoClass(ReportBean.java:1504) at com.wabacus.config.component.application.report.ReportBean.doPostLoad(ReportBean.java:1431) at com.wabacus.config.component.container.AbsContainerConfigBean.doPostLoad(AbsContainerConfigBean.java:435) at com.wabacus.config.component.container.panel.TabsPanelBean.doPostLoad(TabsPanelBean.java:191) at com.wabacus.config.component.container.AbsContainerConfigBean.doPostLoad(AbsContainerConfigBean.java:435) at com.wabacus.config.component.container.page.PageBean.doPostLoad(PageBean.java:330) at com.wabacus.config.ConfigLoadManager.loadAllReportSystemConfigs(ConfigLoadManager.java:203) at com.wabacus.WabacusServlet.loadReportConfigFiles(WabacusServlet.java:112) at com.wabacus.WabacusServlet.contextInitialized(WabacusServlet.java:79)
原框架,项目运行起来后,PermGen使用情况
reload 2次后PermGen使用情况
类装载和卸载情况
reload 4次后PermGen使用情况
类装载和卸载情况
第四次reload后,出现了PermGen的垃圾回收,大量类被卸载,从堆的dump文件上也可以看出,原框架大量类没有被卸载。下面是对比图
原框架。reload2次后
修改后,reload4次后
总结
在reload和tomcat关闭时,内存泄漏的提示也没有了,从之前的提示上来看,个人感觉是线程为正常退出,导致classloader不能够被卸载,从而导致classloader加载的类不能被卸载,导致方法区内存溢出。
对于本文讨论的两个线程,感觉处于sleep状态的线程,在一段时间后会自动退出,影响没有那么大,而waiting状态的进程,是不可能退出的,在reload时会导致内存的泄漏。而在tomcat关闭后能够顺利退出,因为两个进程都是daemon进程,在程序退出后,线程也就退出了。但是,在这里,两个线程应不应该是daemon线程,待后续深入两个线程到底干了什么后在讨论。
相关文章推荐
- MyEclipse Web Project转Eclipse Dynamic Web Project
- 插件管理框架 for Delphi(一)
- 使用CSS框架布局的缺点和优点小结
- 一起动手编写Android图片加载框架
- 基于.NET平台常用的框架和开源程序整理
- MyEclipse连接Mysql数据库的方法(一)
- MyEclipse常用配置图文教程
- 列举PHP的Yii 2框架的开发优势
- Windows窗体的.Net框架绘图技术实现方法
- 浅谈JavaScript 框架分类
- 轻量级javascript 框架Backbone使用指南
- javascript实现框架高度随内容改变的方法
- JS刷新框架外页面七种实现代码
- 超赞的动手创建JavaScript框架的详细教程
- 深入探讨前端框架react
- js验证框架实现代码分享
- jQuery的框架介绍
- 简单介绍不用库(框架)自己写ajax
- 自用js开发框架小成 学习js的朋友可以看看
- 利用ASP.NET MVC+EasyUI+SqlServer搭建企业开发框架