您的位置:首页 > 编程语言 > Java开发

wabacus框架在Myeclipse reload过程中方法区溢出问题讨论

2016-06-20 17:10 507 查看
问题解析

总结

问题解析

(下面的文章都是基于个人的知识,由于本人是个菜鸟,欢迎指正)

现在手上有一个比较简单的信息管理系统的小项目,最开始立项的时候我还没有来到学校,已经定下来采用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线程,待后续深入两个线程到底干了什么后在讨论。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息