2015-06-01 2 views
2

Я разрабатываю веб-приложение, которое берет zip-файл, загружается пользователем, разархивирует его на сервере и обрабатывает файлы. Он работает как шарм, когда zip-файл не слишком большой (20-25 МБ), но если файл примерно или меньше (50 МБ), он создает OutOfMemoryError.OutOfMemoryError on tomcat7

Я попытался увеличить пул распределения памяти в java, добавив export CATALINA_OPTS="-Xmx1024M" в startup.sh в tomcat7, но ошибка по-прежнему сохраняется.

AFAIK, проблема в распаковке ZIP-файла. top показывает, что tomcat использует 800 МБ памяти при извлечении 50 МБ файла. Есть ли какое-либо решение, позволяющее загружать до 200 МБ, эффективно используя доступную память?

Код для разархивирования выглядит следующим образом:

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 относительно этого исключения.

Заранее спасибо.

EDIT Код для DiskFileItem в Upload.jsp

//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  
} 
    } 
} 
+0

Кажется, вы используете Java 7. Java 8 справляется с этими проблемами самостоятельно, без каких-либо дополнительных настроек пользователем. –

+1

Вы должны обработать исключение во внутреннем цикле. Вы даже не закрываете файлы, если происходит что-то плохое. –

+0

Вы пытаетесь обрабатывать огромный файл с такой ошибкой памяти. –

ответ

2

Вопрос не в том, что вы опубликовали. корневая коса находится в:

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, поскольку почтовый индекс не использует такую ​​вещь


Update: Из кода вы публикуемые, кажется, что ваш код нормально.Но 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 ни перед дополнительной 8-32 Kb

  2. Дайте больше памяти для процесса JVM: -Xmx2g например

  3. Марка убедитесь, что у вас нет лишних ссылок на FileItem, так как в вашей текущей конфигурации они потребляют много памяти.

  4. Если OOM повторится, возьмите кучу. Вы можете использовать флаг JVM -XX:+HeapDumpOnOutOfMemoryError, чтобы автоматически создать для вас кучу. Затем вы можете использовать анализатор дампа кучи (например, Eclipse MAT), чтобы проверить, кто выделяет столько памяти и где он выделяется.

+0

Кто-то, вероятно, просто попытался загрузить файл снова, а tomcat не смог выделить память для этого файла из-за проблемы с памятью вероятно, вызвано извлечением zip-файла (или что-либо еще, что произошло на этом сервере). Неверный указатель. – defectus

+0

Знаете ли вы хотя бы некоторую java? ** Знаете ли вы, что такое stacktrace? ** Вы знакомы с тем, как java управляет памятью? –

+1

Обратите внимание, что это stacktrace указывает на API загрузки apache, но не на код, который выращивается на дому. По-видимому, по какой-то причине загрузка файла хочет полностью загрузить весь файл в память, чтобы получить размер файла. – Gimby

0

8MB Выделение для каждой записи молнии, кажется, просто пальцем в воздухе подхода. Попробуйте использовать меньшие буферы, скажем, не более 1 КБ. Сбор мусора не окуривается непрерывно.

Попробуйте использовать этот подход:

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); 
} 
+1

Совершенно неправильно. Он выделяет 8 ** KB ** не МБ !!!Также, когда память закончится, GC запустится, поэтому ваш rremark о GC является простым глупо. Сокращение буфера не поможет, но это ухудшит производительность. Оптимальный размер byffer находится между 4-8k. Также большинство (если не все) системных классов JDK используют буфер 8k –

+0

My bad. Тем не менее выделение 8kB на каждый цикл выглядит неэффективным. И поскольку GC запускается в его собственном потоке, может случиться так, что к тому времени GC закончит выделение потока, уже используемого для освобождения памяти. – defectus

+1

Ну, GC * останавливает мир *, так что этого не может быть –

0

Похоже, ваш while цикл делает слишком много создания памяти.

Проверьте количество раз, которое оно принимает, чтобы решить.

Главная эта линия ниже является причиной:

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

Вы можете попытаться сократить 1024 на что-то вроде 10 и посмотреть, если это все-таки happeneds.
Также проверьте размер файла.

+0

Создание нового буфера на каждой итерации действительно глупо, но оно не может вызвать ошибку OOM, так как GC сможет собирать предыдущий буфер. Уменьшение размера буфера не поможет - это только ухудшит производительность –

+0

, по-прежнему необходимо его проверить, поскольку я вижу его здесь –

1

Проблема, когда пользователь загружает файл почтового индекса, весь почтовый файл получать чтения в памяти, из трассировки стека ошибки вызывается при выполнении вызова

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. 

Idealy пользователь не должен иметь возможность загружать файлы любого размера, должно быть не более предельного размера учитывая ваши способности сервера.