【问题标题】:OutOfMemoryError on tomcat7tomcat7上的OutOfMemoryError
【发布时间】:2015-08-14 15:27:43
【问题描述】:

我正在开发一个网络应用程序,它需要一个 zip 文件,由用户上传,在服务器上解压缩并处理文件。当 zip 文件不是太大(20-25MB)时,它就像一个魅力,但如果文件大约或超过(50MB),它会产生 OutOfMemoryError。

我尝试通过添加来增加 java 最大内存分配池 export CATALINA_OPTS="-Xmx1024M" tomcat7中的startup.sh,但错误依旧存在。

AFAIK,问题在于解压缩 .zip 文件。 top 显示在提取 50MB 文件期间,tomcat 使用了 800MB 内存。是否有任何解决方案可以在有效利用可用内存的同时实现高达 ~200MB 的上传?

解压代码如下:

package user;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class unzip {

public void unzipFile(String filePath, String oPath)
{

    FileInputStream fis = null;
    ZipInputStream zipIs = null;
    ZipEntry zEntry = null;
    try {
        fis = new FileInputStream(filePath);
        zipIs = new ZipInputStream(new BufferedInputStream(fis));
        while((zEntry = zipIs.getNextEntry()) != null){
            try{
                byte[] tmp = new byte[8*1024];
                FileOutputStream fos = null;
                String opFilePath = oPath+zEntry.getName();
                System.out.println("Extracting file to "+opFilePath);
                fos = new FileOutputStream(opFilePath);
                int size = 0;
                while((size = zipIs.read(tmp)) != -1){
                    fos.write(tmp, 0 , size);
                }
                fos.flush();
                fos.close();
            }catch(Exception ex){

            }
        }
        zipIs.close();
        fis.close();
    } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}
}

错误代码如下:

HTTP Status 500 - javax.servlet.ServletException:      java.lang.OutOfMemoryError: Java heap space

type Exception report

message javax.servlet.ServletException: java.lang.OutOfMemoryError: Java heap space

description The server encountered an internal error that prevented it from fulfilling this request.

exception

org.apache.jasper.JasperException: javax.servlet.ServletException: java.lang.OutOfMemoryError: Java heap space
    org.apache.jasper.servlet.JspServletWrapper.handleJspException(JspServletWrapper.java:549)
    org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:455)
    org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:390)
org.apache.jasper.servlet.JspServlet.service(JspServlet.java:334)
javax.servlet.http.HttpServlet.service(HttpServlet.java:727)

root cause

javax.servlet.ServletException: java.lang.OutOfMemoryError: Java heap space
    org.apache.jasper.runtime.PageContextImpl.doHandlePageException(PageContextImpl.java:916)
    org.apache.jasper.runtime.PageContextImpl.handlePageException(PageContextImpl.java:845)
    org.apache.jsp.Upload_jsp._jspService(Upload_jsp.java:369)
org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
    org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:432)
org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:390)
org.apache.jasper.servlet.JspServlet.service(JspServlet.java:334)
javax.servlet.http.HttpServlet.service(HttpServlet.java:727)

root cause

java.lang.OutOfMemoryError: Java heap space
    org.apache.commons.io.output.ByteArrayOutputStream.toByteArray(ByteArrayOutputStream.java:322)
    org.apache.commons.io.output.DeferredFileOutputStream.getData(DeferredFileOutputStream.java:213)
    org.apache.commons.fileupload.disk.DiskFileItem.getSize(DiskFileItem.java:289)
org.apache.jsp.Upload_jsp._jspService(Upload_jsp.java:159)
org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
    org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:432)
org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:390)
org.apache.jasper.servlet.JspServlet.service(JspServlet.java:334)
javax.servlet.http.HttpServlet.service(HttpServlet.java:727)

note The full stack trace of the root cause is available in the Apache Tomcat/7.0.52 (Ubuntu) logs.
Apache Tomcat/7.0.52 (Ubuntu)

令人惊讶的是,catalina.out 文件中没有关于此异常的任何内容。

提前致谢。

编辑 Upload.jsp 中 DiskFileItem 的代码

//necessary imports go here
File file ;
int maxFileSize = 1000 * 1000 * 1024;
int maxMemSize = 1000 * 1024;
ServletContext context = pageContext.getServletContext();
String filePath = context.getInitParameter("file-upload");
String contentType = request.getContentType();
if(contentType != null)
{
  if ((contentType.indexOf("multipart/form-data") >= 0)) 
  {
  DiskFileItemFactory factory = new DiskFileItemFactory();
  factory.setSizeThreshold(maxMemSize);
  factory.setRepository(new File("/tmp/"));
  ServletFileUpload upload = new ServletFileUpload(factory);
  upload.setSizeMax( maxFileSize );
  try{ 
     List fileItems = upload.parseRequest(request);
     Iterator i = fileItems.iterator();
     while (i.hasNext ()) 
     {

        FileItem fi = (FileItem)i.next();
        if ( !fi.isFormField () )   
        {
           String fieldName = fi.getFieldName();
           String fileName = fi.getName();
           if(fileName.endsWith(".zip")||fileName.endsWith(".pdf")||fileName.endsWith(".doc")||fileName.endsWith(".docx")||fileName.endsWith(".ppt")||fileName.endsWith(".pptx")||fileName.endsWith(".html")||fileName.endsWith(".htm")||fileName.endsWith(".epub")||fileName.endsWith(".djvu"))
           {
              boolean isInMemory = fi.isInMemory();
              long sizeInBytes = fi.getSize();            
              new File(filePath+fileName).mkdir();
              filePath = filePath+fileName+"/";
              file = new File( filePath + fileName.substring( fileName.lastIndexOf("/"))) ;
              fi.write(file);
              String fileExtension = FilenameUtils.getExtension(fileName);
              if(fileExtension.equals("zip"))
              {
                 System.out.println("In zip.");
                 unzip mfe = new unzip();
                 mfe.unzipFile(filePath+fileName,filePath);
                 File zip = new File(filePath+fileName);
                 zip.delete();
              }
              File corePath = new File(filePath);
              int count=0;
           //some more processing
           }
        }
     }
  }
  catch(Exception e)
  {
     //exception handling goes here      
}
  }
}

【问题讨论】:

  • 看来您使用的是 Java 7。Java 8 自行处理此类问题,无需用户进行任何额外配置。
  • 您应该真正处理内部循环中的异常。如果发生不好的事情,您甚至不会关闭文件
  • 您正在尝试处理大文件,因此出现此类内存错误。
  • @mmc18 处理大文件可能会消耗大量内存;但它不一定必须。!这在很大程度上取决于 whathow 事情是如何完成的。长话短说-您的评论没有帮助;除此之外:做出如此大胆的声明根本不是基于事实。

标签: java jsp tomcat memory


【解决方案1】:

问题不在于您发布的解压缩代码。根源在:

java.lang.OutOfMemoryError: Java heap space
    org.apache.commons.io.output.ByteArrayOutputStream.toByteArray(ByteArrayOutputStream.java:322)
    org.apache.commons.io.output.DeferredFileOutputStream.getData(DeferredFileOutputStream.java:213)
    org.apache.commons.fileupload.disk.DiskFileItem.getSize(DiskFileItem.java:289)

你注意到ByteArrayOutputStream.toByteArray 了吗?因此,您似乎正在写信给增长太多的ByteArrayOutputStream。请找到并发布使用此ByteArrayOutputStream 的代码,因为您的邮政编码不使用这种东西


更新: 从您发布的代码看来,您的代码没问题。但是FileItem.getSize() 调用做了一些讨厌的事情:

283   public long getSize() {
284        if (size >= 0) {
285            return size;
286        } else if (cachedContent != null) {
287            return cachedContent.length;
288        } else if (dfos.isInMemory()) {
289            return dfos.getData().length;
290        } else {
291            return dfos.getFile().length();
292        }
293    }

如果文件项的数据存储在内存中 - 它调用getData(),后者调用toByteArray()

209    public byte[]  [More ...] getData()
210    {
211        if (memoryOutputStream != null)
212        {
213            return memoryOutputStream.toByteArray();
214        }
215        return null;
216    }

依次分配一个新数组:

317    public synchronized byte[] toByteArray() {
318        int remaining = count;
319        if (remaining == 0) {
320            return EMPTY_BYTE_ARRAY; 
321        }
322        byte newbuf[] = new byte[remaining];
           //Do stuff
333        return newbuf;
334    }

因此,在短时间内,您的内存消耗是正常的两倍。

我会推荐你​​:

  1. maxMemSize 设置为 no-more 8-32 Kb

  2. 给 JVM 进程更多内存:例如-Xmx2g

  3. 确保您没有对FileItems 进行任何不必要的引用,因为在当前配置中它们会消耗大量内存。

  4. 如果 OOM 再次发生,请进行堆转储。您可以使用-XX:+HeapDumpOnOutOfMemoryError JVM 标志自动为您创建堆转储。然后,您可以使用堆转储分析器(例如 Eclipse MAT)来检查谁分配了这么多内存以及分配的位置。

【讨论】:

  • 可能有人刚刚尝试再次上传文件,但由于 zip 文件提取(或该服务器上发生的任何其他问题)可能导致的内存问题,tomcat 无法为该文件分配内存。错误的指针。
  • 你至少知道一些 java 吗? 你知道stacktrace是什么吗?你熟悉java如何管理内存吗?
  • 请注意,此堆栈跟踪指向 apache commons fileupload API,而不是本地开发的代码。显然出于某种原因,文件上传希望将整个文件放入内存中,以便能够获取文件大小。
  • @Gimby - 我猜 fteh 堆栈跟踪的其余部分在服务器日志中可见,正如最后一条消息所说:)
  • Apache 文档。如下所述,因此减小 maxMemSize 会将文件存储在磁盘而不是内存中,从而以数据访问速度进行交易。我对吗? public void setSizeThreshold(int sizeThreshold) Sets the size threshold beyond which files are written directly to disk.我已经减少了,现在错误没有出现:| .我会尝试其他建议,以防它回来。
【解决方案2】:

问题是当用户上传 zip 文件时,整个 zip 文件在内存中被读取,从堆栈跟踪中调用

DiskFileItem.getSize()

从 DiskFileItem 的源代码, DiskFileItem.getSize() 是先获取所有数据,

public long getSize() {
284        if (size >= 0) {
285            return size;
286        } else if (cachedContent != null) {
287            return cachedContent.length;
288        } else if (dfos.isInMemory()) {
289            return dfos.getData().length;
290        } else {
291            return dfos.getFile().length();
292        }
293    }

通过查看 DeferredFileOutputStream.getDate() 的文档

Returns either the output file specified in the constructor or the temporary file created or null.
If the constructor specifying the file is used then it returns that same output file, even when threashold has not been reached.
If constructor specifying a temporary file prefix/suffix is used then the temporary file created once the threashold is reached is returned If the threshold was not reached then null is returned.

Returns:
    The file for this output stream, or null if no such file exists.

理想情况下,不应允许用户上传任何大小的文件,鉴于您的服务器容量,应该有最大大小限制。

【讨论】:

    【解决方案3】:

    为每个 zip 条目分配 8MB 似乎只是悬而未决的方法。尝试使用较小的缓冲区,比如不超过 1kb。垃圾收集不会连续发生。

    尝试使用这种方法:

    int BUFFER_SIZE = 1024;
    int size;
    byte[] buffer = new byte[BUFFER_SIZE];
    
    ...
    FileOutputStream out = new FileOutputStream(path, false);
    BufferedOutputStream fout = new BufferedOutputStream(out, BUFFER_SIZE);
    
    while ( (size = zin.read(buffer, 0, BUFFER_SIZE)) != -1 ) {
       fout.write(buffer, 0, size);
    }
    

    【讨论】:

    • 完全错误。他分配的是 8 KB 而不是 MB!!!此外,当内存用完时,GC 将启动,因此您对 GC 的评论简直是愚蠢的。减少缓冲区无济于事,但会降低性能。最佳的 byffer 大小在 4-8k 之间。大多数(如果不是全部)JDK 系统类也使用 8k 的缓冲区
    • 我的错。仍然在每个循环上分配 8kB 看起来效率低下。由于 GC 在它自己的线程中运行,可能会在 GC 完成分配线程时已经使用释放的内存。
    • 好吧,GC 停止世界,所以这不可能发生
    【解决方案4】:

    您的 while 循环似乎创建了太多内存。

    检查它发生的次数来决定。

    下面这行主要是原因:

    byte[] tmp = new byte[8*1024];
    

    您可以尝试将 1024 减少到 10 左右,看看它是否仍然发生。
    还要检查文件大小。

    【讨论】:

    • 在每次迭代时创建一个新的缓冲区确实很愚蠢,但它不会导致 OOM 错误,因为 GC 将能够收集以前的缓冲区。减小缓冲区大小无济于事 - 只会降低性能
    • 它仍然需要检查,因为我从这里看到它
    猜你喜欢
    • 2012-09-24
    • 1970-01-01
    • 1970-01-01
    • 2023-03-21
    • 1970-01-01
    • 1970-01-01
    • 2012-12-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多