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

本文分析了Wabacus框架在项目重部署时出现的内存泄漏问题,特别是两个线程未能正常退出导致的内存占用问题,并提出了解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  1. 问题解析
  2. 总结

    1. 问题解析
      (下面的文章都是基于个人的知识,由于本人是个菜鸟,欢迎指正)
      现在手上有一个比较简单的信息管理系统的小项目,最开始立项的时候我还没有来到学校,已经定下来采用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命令查看该进程中的所有线程状态。如下图
7748 是两个线程所在的进程id
下图是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)
            {
                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次后
这里写图片描述

  1. 总结
    在reload和tomcat关闭时,内存泄漏的提示也没有了,从之前的提示上来看,个人感觉是线程为正常退出,导致classloader不能够被卸载,从而导致classloader加载的类不能被卸载,导致方法区内存溢出。
    对于本文讨论的两个线程,感觉处于sleep状态的线程,在一段时间后会自动退出,影响没有那么大,而waiting状态的进程,是不可能退出的,在reload时会导致内存的泄漏。而在tomcat关闭后能够顺利退出,因为两个进程都是daemon进程,在程序退出后,线程也就退出了。但是,在这里,两个线程应不应该是daemon线程,待后续深入两个线程到底干了什么后在讨论。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值