`
learnworld
  • 浏览: 168337 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

Struts文件上传报OutOfMemoryError问题分析

阅读更多

好久没有更新博客了,最近项目也接近尾声了,今天记录一个case处理过程。

 

一、问题描述

1. 异常信息

 

java.lang.OutOfMemoryError: Java heap space
	java.io.ByteArrayOutputStream.<init>(Unknown Source)
	org.apache.commons.fileupload.DeferredFileOutputStream.<init>(DeferredFileOutputStream.java:131)
	org.apache.commons.fileupload.DefaultFileItem.getOutputStream(DefaultFileItem.java:558)
	org.apache.commons.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:406)
	org.apache.struts.upload.CommonsMultipartRequestHandler.handleRequest(CommonsMultipartRequestHandler.java:193)
	org.apache.struts.util.RequestUtils.populate(RequestUtils.java:443)
	org.apache.struts.action.RequestProcessor.processPopulate(RequestProcessor.java:804)
	org.apache.struts.action.RequestProcessor.process(RequestProcessor.java:203)
	org.apache.struts.action.ActionServlet.process(ActionServlet.java:1196)
	org.apache.struts.action.ActionServlet.doPost(ActionServlet.java:432)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:637)
	com.xxxx.ui.framework.ActionServlet.service(ActionServlet.java:138)
	javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
	com.xxxx.ui.framework.SessionFilter.doFilter(SessionFilter.java:89)

 

 

2. 问题重现过程



 

1) 页面上有一个文件导入功能,可以导入IP信息列表,用户可以通过">>"和“<<”按钮对导入的信息进行添加和删除。

2) 当从文件中导入5000条IP信息时,数据导入过程可以从正常完成,导入后的数据也能正常显示。

3) 当将导入的5000条数据全部删除时,抛出上述OutOfMemoryError异常。

 

二、问题分析

第一次QA发现这个问题时,我想当然以为这是由于导入的数据量太大,导致内存用尽,从而报出这个错误。因为之前版本也有这个问题,所以优先级比较低。直到最近项目接近尾声,经老板提醒,IP地址信息每个存储都小于128byte, 5000个IP地址信息总共也只有500k, 为什么会导致内存耗尽呢? 此事背后一定隐藏着一个天大的秘密!

 

三、分析过程

1) 通过VisualVM连接tomcat进程,观察内存使用情况。

 

 通过几次测试,发现每次提交删除大量IP地址数据时,内存会突然增大,从而导致OutOfMemoryError,而过段时间,通过垃圾回收,内存会重新回收。

 

2)排查代码

我在代码中处理IP信息删除的Action里设置断点,却发现在到达这个断点之前,已经抛出OutOfMemoryError,从而排除由于代码中创建大量对象导致OutOfMemoryError的可能性。

 

3)  通过观察异常堆栈,初步推断: Struts在处理Multipart request时,调用fileupload组件。 fileupload组件在创建流的过程中,内存不足导致OutOfMemoryError异常。

通过查看jsp文件,发现提交的form定义如下:

<html:form action="/saveSMTPConn.action"  enctype="multipart/form-data">

由此可见Struts调用fileupload来处理multipart request可能有问题。 在Apache官网查阅了issue列表,只找到一个类似的issue: STR-1857. 这个issue中提到fileupload模块本身有缺陷,但没有提及详细原因。所以决定从源码入手进行分析。

 

四、源码分析

从官网上下载了Struts1.2.7和fileupload1.0的源码,导入IDE中开始调试。

1) 找到Struts调用fileupload进行Multipart Request解析的代码:

 

    public void handleRequest(HttpServletRequest request)
            throws ServletException {

        // Get the app config for the current request.
        ModuleConfig ac = (ModuleConfig) request.getAttribute(
                Globals.MODULE_KEY);

        // Create and configure a DIskFileUpload instance.
        DiskFileUpload upload = new DiskFileUpload();
        // The following line is to support an "EncodingFilter"
        // see http://nagoya.apache.org/bugzilla/show_bug.cgi?id=23255
        upload.setHeaderEncoding(request.getCharacterEncoding());
        // Set the maximum size before a FileUploadException will be thrown.
        upload.setSizeMax(getSizeMax(ac));
        // Set the maximum size that will be stored in memory.
        upload.setSizeThreshold((int) getSizeThreshold(ac));
        // Set the the location for saving data on disk.
        upload.setRepositoryPath(getRepositoryPath(ac));

        // Create the hash tables to be populated.
        elementsText = new Hashtable();
        elementsFile = new Hashtable();
        elementsAll = new Hashtable();

        // Parse the request into file items.
        List items = null;
        try {
            items = upload.parseRequest(request);
        } catch (DiskFileUpload.SizeLimitExceededException e) {
            // Special handling for uploads that are too big.
            request.setAttribute(
                    MultipartRequestHandler.ATTRIBUTE_MAX_LENGTH_EXCEEDED,
                    Boolean.TRUE);
            return;
        } catch (FileUploadException e) {
            log.error("Failed to parse multipart request", e);
            throw new ServletException(e);
        }

        // Partition the items into form fields and files.
        Iterator iter = items.iterator();
        while (iter.hasNext()) {
            FileItem item = (FileItem) iter.next();

            if (item.isFormField()) {
                addTextParameter(request, item);
            } else {
                addFileParameter(item);
            }
        }
    }
 从上述代码可以看到,首先定义DiskFileUpload对象,接着给这个对象设定参数,然后upload.parseRequest(request)返回解析后的参数列表(FileItem List) ,最后将结果放入相应的集合中。可见Struts调用fileupload主要用于解析header中的parameter,供自己后续处理。
 
2) 查看upload.parseRequest(request)的具体处理过程。
    public List /* FileItem */ parseRequest(HttpServletRequest req)
        throws FileUploadException
    {
        if (null == req)
        {
            throw new NullPointerException("req parameter");
        }

        ArrayList items = new ArrayList();
        String contentType = req.getHeader(CONTENT_TYPE);

        ...

        try
        {
            int boundaryIndex = contentType.indexOf("boundary=");
            if (boundaryIndex < 0)
            {
                throw new FileUploadException(
                        "the request was rejected because "
                        + "no multipart boundary was found");
            }
            byte[] boundary = contentType.substring(
                    boundaryIndex + 9).getBytes();

            InputStream input = req.getInputStream();

            MultipartStream multi = new MultipartStream(input, boundary);
            multi.setHeaderEncoding(headerEncoding);

            boolean nextPart = multi.skipPreamble();
            while (nextPart)
            {
                Map headers = parseHeaders(multi.readHeaders());
                String fieldName = getFieldName(headers);
                if (fieldName != null)
                {
                    String subContentType = getHeader(headers, CONTENT_TYPE);
                    if (subContentType != null && subContentType
                                                .startsWith(MULTIPART_MIXED))
                    {
                        ...
                    }
                    else
                    {
                        if (getFileName(headers) != null)
                        {
                             ....
                        }
                        else
                        {
                            // A form field. Important here!!!
                            // 1. 开始处理每个form field,为每个field创建一个FileItem
                            FileItem item = createItem(headers, true);
                            // 2. 为每个FileItem创建OutStream
                            OutputStream os = item.getOutputStream();
                            try
                            {
                                // 3. 读取Form Field数据,写入OutStream中
                                multi.readBodyData(os);
                            }
                            finally
                            {
                                os.close();
                            }
                            // 4. 将Form Field处理结果加入返回结果集中
                            items.add(item);
                        }
                    }
                }
                else
                {
                    // Skip this part.
                    multi.discardBodyData();
                }
                nextPart = multi.readBoundary();
            }
        }
        catch (IOException e)
        {
            throw new FileUploadException(
                "Processing of " + MULTIPART_FORM_DATA
                    + " request failed. " + e.getMessage());
        }

        return items;
    }
 代码的注释中我已经标明了处理过程中的关键四步,既然报OutOfMemoryError,肯定和内存分配有关系。
 
3) 查看第二步中为每个FileItem创建OutStream的代码。
    public OutputStream getOutputStream()
        throws IOException
    {
        if (dfos == null)
        {
            File outputFile = getTempFile();
            dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
        }
        return dfos;
    }
这段代码中创建了临时文件,并同时创建了DeferredFileOutputStream。 查看DeferredFileOutputStream的构造函数如下:
    public DeferredFileOutputStream(int threshold, File outputFile)
    {
        super(threshold);
        this.outputFile = outputFile;

        memoryOutputStream = new ByteArrayOutputStream(threshold);
        currentOutputStream = memoryOutputStream;
    }
可以看到, 这里创建了ByteArrayOutputStream, 并分配threshold大小的内存,threshold的默认值为256K。当参数值小于256k时,会被存储在内存中;当参数值大于等于256k时,会被存储在临时文件中。
 
五、结果分析
通过上面代码可以看出,FileUpload为每个FormField分配了256k的内存,用于存储parameter value,供后续与Struts框架数据交换。 如果Form表单中有1000个参数,将会使用256M内存。 我测试时导入的IP地址信息数量在4000左右,将消耗1G左右的内存,所以导致出现OutOfMemoryError异常。
 
我查阅了FileUpload的issue列表,找到了相关的case: FILEUPLOAD-59. 可以看到这个问题在FileUpload 1.1-dev版本中已经修复。 我下载了FileUpload1.1.1版本的源码,看到DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD初始化为10240byte(10k), 相比原来的256k默认值已经大大减少。
 通过将Struts升级到1.3.10(包含的FileUpload版本为1.1.1),该问题解决。
 
 
ps. 维护遗留系统的孩纸你伤不起,到处都是坑!



 
 

 

 

  • 大小: 56.7 KB
  • 大小: 64.7 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics