`
learnworld
  • 浏览: 169724 次
  • 性别: 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
分享到:
评论

相关推荐

    struts文件上传详解

    在Struts中,文件上传是一个常见的需求,它允许用户从客户端上传文件到服务器。Apache Commons FileUpload库是Struts实现文件上传功能的核心组件。 首先,`DiskFileUpload`类是Apache Commons FileUpload库的主要类...

    MAT解析hprof内存溢出分析工具OutOfMemoryError-java程序开发

    MAT(Memory Analyzer Tool)是IBM提供的一款强大的Java内存分析工具,它专为解决此类问题而设计,帮助开发者深入理解内存消耗,有效地定位内存泄漏和性能瓶颈。 MAT的使用方法和功能详解: 1. **数据获取**:首先...

    ant编译时抛出OutOfMemoryError.doc

    在使用Apache Ant构建Java项目时,可能会遇到一个常见的问题,即`OutOfMemoryError`。这个错误通常发生在编译大量Java源文件时,由于资源耗尽,JVM无法分配足够的内存来执行任务。`OutOfMemoryError`是Java运行时...

    junrar的OutOfMemoryError错误解决源码

    总结,junrar的`OutOfMemoryError`问题是由于内存管理不当造成的,解决之道在于采用更高效的内存使用策略,如流式处理和内存映射文件。通过理解和应用这些技术,开发者可以提高junrar的性能,避免在处理大文件时出现...

    java虚拟机OutOfMemoryError:Java heap space堆dump文件

    java虚拟机OutOfMemoryError:Java heap space堆dump文件,可以直接用来分析。

    java.lang.OutOfMemoryError: Java heap space 解决方法

    - **日志记录与分析**:开启详细的垃圾回收日志,通过分析日志了解垃圾回收的频率和效果,以及可能存在的问题。 #### 结论 `java.lang.OutOfMemoryError: Java heap space`错误通常是由于内存配置不当、代码设计不...

    大文件分片合并上传 feign调用

    综上所述,"大文件分片合并上传 feign调用"是Java开发中解决大文件传输问题的一种有效方法,它结合了分片、Feign调用和文件合并技术,既提高了系统性能,又降低了内存消耗,确保了大文件传输的可靠性和效率。...

    内存泄露分析工具(IBM HeapAnalyzer 和 Pattern Modeling and Analysis )

    内存泄露是Java应用程序中常见的问题,它会导致程序性能下降,甚至最终引发`java/lang/OutOfMemoryError`。IBM HeapAnalyzer和Pattern Modeling and Analysis (PMA)是两种强大的工具,专门用于诊断和解决这类问题。 ...

    遭遇OutOfMemoryError

    在IT领域,尤其是在Java开发中,`OutOfMemoryError`是一个常见的问题,通常表明程序在运行过程中耗尽了分配给它的内存资源。这个问题在本案例中发生在网店系统的升级后,经过压力测试,系统突然崩溃,抛出了`...

    java错误处理:java.lang.OutOfMemoryError: Java heap space

    - 描述提到了对这个问题的相关资料进行整理,这意味着该文档将提供如何识别、分析并解决此类问题的方法。 #### 详细解析 **异常出现的原因:** 1. **内存分配不足**:默认情况下,JVM启动时分配的堆内存可能不...

    Myeclipse下java.lang.OutOfMemoryError Java heap space的解决

    #### 一、问题分析 1. **异常含义**: - `java.lang.OutOfMemoryError: Java heap space`表示Java程序在运行过程中耗尽了所有可用的堆内存空间。 - 当程序创建新的对象或者分配内存时,如果无法在现有的堆内存中...

    解决OutOfMemoryError内存溢出

    在Java开发过程中,我们经常会遇到`java.lang.OutOfMemoryError`(简称OOM)的问题。这个问题的发生主要是由于JVM内存不足或程序中存在内存泄漏所引起的。本文将深入探讨OOM产生的原因以及如何有效地解决这一问题。 ...

    java.lang.OutOfMemoryError处理错误

    java.lang.OutOfMemoryError处理错误 java.lang.OutOfMemoryError是Java虚拟机(JVM)中的一种常见错误,...java.lang.OutOfMemoryError是Java虚拟机中的一种常见错误,解决这种错误需要根据实际情况进行分析和解决。

    tomcat 出现 OutOfMemoryError 的解决方法

    针对 Tomcat 出现 `java.lang.OutOfMemoryError: PermGen space` 和 `java.lang.OutOfMemoryError: Java heap space` 的问题,通过调整 JVM 的内存配置以及优化应用本身,可以有效避免内存溢出的发生。同时,对于...

    问题分析:java.lang.OutOfMemoryError unable to create new native thre

    【Java虚拟机内存溢出分析】:当遇到`java.lang.OutOfMemoryError: unable to create new native thread`错误时,这通常表示系统无法为新的Java线程分配足够的内存,即操作系统层面的资源耗尽,而非Java堆内存不足。...

    解决OutOfMemoryError: PermGen space

    解决“OutOfMemoryError: PermGen space”问题虽然过程可能比较痛苦,但是一旦找到正确的方法,问题就能得到解决。本文提供的解决方案包括调整PermGen space的大小、优化代码、使用JConsole监控内存等,希望能帮助...

    java.lang.OutOfMemoryError解决办法

    Java中的`java.lang.OutOfMemoryError`是一种常见的运行时错误,通常表示应用程序在尝试分配内存时遇到了问题。根据提供的信息,这个错误主要涉及到两个方面:`PermGen space`和`Java heap`,并且与Tomcat服务器相关...

    找到该死的OutOfMemoryError.docx

    为了解决根本问题,我们需要让JVM在出现`OutOfMemoryError`时生成堆转储(Heap Dump)文件。通过添加`-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/error`,当错误发生时,JVM会将内存快照保存到指定...

Global site tag (gtag.js) - Google Analytics