java.lang.OutOfMemoryError на HttpServer при загрузке больших данных - PullRequest
0 голосов
/ 26 октября 2011

У меня есть встроенный Java 6 HttpServer. Он имеет дескриптор, который позволяет клиентам загружать большой текстовый файл. Проблема в том, что когда на сервере более 10 клиентов одновременно, я получаю исключение из памяти. Я уверен, что проблема связана с Http-сервером.

   HttpServer m_server = HttpServer.create(new InetSocketAddress(8080), 0);
   m_server.createContext("/DownloadFile", new DownloadFileHandler() );

   public class DownloadFileHandler implements HttpHandler {

         private static byte[] myFile = new String("....................").getBytes(); //string about 8M

         @Override
         public void handle(HttpExchange exchange) throws IOException {
                exchange.sendResponseHeaders(HTTP_OK, myFile .length);                 OutputStream responseBody = exchange.getResponseBody();
                responseBody.write(myFile );
                responseBody.close();
         } 
   }

Теперь я получаю исключение:

java.lang.OutOfMemoryError: Java heap space 
at java.nio.HeapByteBuffer.<init>(Unknown Source)
at java.nio.ByteBuffer.allocate(Unknown Source)
at sun.net.httpserver.Request$WriteStream.write(Unknown Source)
at sun.net.httpserver.FixedLengthOutputStream.write(Unknown Source) 
at java.io.FilterOutputStream.write(Unknown Source) 
at sun.net.httpserver.PlaceholderOutputStream.write(Unknown Source) 
at com.shunra.javadestination.webservices.DownloadFileHandler.handle(Unknown Source) 
at com.sun.net.httpserver.Filter$Chain.doFilter(Unknown Source) 
at sun.net.httpserver.AuthFilter.doFilter(Unknown Source) 
at com.sun.net.httpserver.Filter$Chain.doFilter(Unknown Source) 
at sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(Unknown Source) 
at com.sun.net.httpserver.Filter$Chain.doFilter(Unknown Source)
at sun.net.httpserver.ServerImpl$Exchange.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(Unknown Source) 
at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.lang.Thread.run(Unknown Source)
Exception in thread "pool-1-thread-24" java.lang.OutOfMemoryError: 

Предложение относительно getBytes () не меняет исключения. я пытался держать статическую ссылку на byte [] вместо того, чтобы создавать его каждый раз. И я все еще получаю то же исключение.

Ответы [ 6 ]

7 голосов
/ 26 октября 2011

Не делайте этого для больших файлов:

byte[] bytesToSend = myFile.getBytes();

Это неэффективно, и вам нужно место в куче для хранения данных всего файла.Вы теряете много места в куче, когда сначала полностью читаете файл, а потом пишете его полностью.

Вместо этого читайте / записывайте данные файла кусками определенного размера из файла непосредственно в ответ.Вы можете написать код самостоятельно или просто использовать служебный класс, такой как IOUtils из Apache Commons IO.

Важно не читать весь файл, прежде чем писать его.Вместо этого делайте это небольшими кусками.Используйте здесь потоки и избегайте всего, что связано с byte [], за исключением буферизации и небольших фрагментов.

Редактировать: Вот код с Apache IO ...

public static void main(String[] args) {
    HttpExchange exchange = ...;
    OutputStream responseBody = null;

    try {
        File file = new File("big-file.txt");
        long bytesToSkip = 4711; //detemine how many bytes to skip

        exchange.sendResponseHeaders(200, file.length() - bytesToSkip);
        responseBody = exchange.getResponseBody();
        skipAndCopy(file, responseBody, bytesToSkip);           
    }
    catch (IOException e) {
        // handle it
    }
    finally {
        IOUtils.closeQuietly(responseBody);
    }
}


private static void skipAndCopy(File src, @WillNotClose OutputStream dest, long bytesToSkip) throws IOException {
    InputStream in = null;

    try {
        in = FileUtils.openInputStream(src);

        IOUtils.skip(in, bytesToSkip);
        IOUtils.copyLarge(in, dest);
    }
    finally {
        IOUtils.closeQuietly(in);
    }
}
5 голосов
/ 26 октября 2011

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

FileReader reader = new FileReader(myFile);
try{
    char buffer[] = new char[4096];
    int numberOfBytes=0;
    while ((numberOfBytes=reader.read(buffer)) != -1){
        responseBody.write(buffer);
    }
}catch(Exception e){
    //TODO do something with the exception.
}finally{
    reader.close();
}
4 голосов
/ 26 октября 2011

При таких больших объемах данных лучше всего передавать данных. Потоковая передача означает, что вы отправляете данные порциями, а не отправляете их сразу. Это более эффективно использует память, потому что вам не нужно хранить все данные в памяти, только их части.

Кроме того, более общий способ возврата данных файла - использовать обычный InputStream вместо Reader.

  • InputStream: используется для чтения любых данных
  • Reader: используется для чтения текстовых данных

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

Вот полное решение:

OutputStream responseBody = null;
try{
  File file = new File("bigggggg-text-file.txt");
  InputStream in = new FileInputStream(file);
  exchange.sendResponseHeaders(HTTP_OK, file.length());
  responseBody = exchange.getResponseBody();
  int read;
  byte buffer[] = new byte[4096];
  while ((read = in.read(buffer)) != -1){
    responseBody.write(buffer, 0, read);
  }
} catch (FileNotFoundException e){
  //uh-oh, the file doesn't exist
} catch (IOException e){
  //uh-oh, there was a problem reading the file or sending the response
} finally {
  if (responseBody != null){
    responseBody.close();
  }
}
4 голосов
/ 26 октября 2011

Используйте потоки, чтобы вам не приходилось записывать все данные сразу.

См. getRequestBody и getResponseBody .Вы захотите открыть свой файл в виде потока и записать байты в соответствующий поток.

0 голосов
/ 26 октября 2011

Не преобразовывать всю строку в байты сразу:

Writer writer = new OutputStreamWriter(responseBody),someEncoding);
try {
  writer.write(myFile);
}
finally {
  writer.close();
}
0 голосов
/ 26 октября 2011

Проблема в вашем коде в том, что myFile.getBytes() создает новый массив для каждого запроса.

Вы можете просто улучшить его, удерживая байтовый массив вместо String:

      private static byte[] bytesToSend = "....................".getBytes(); //string about 8M

     @Override
     public void handle(HttpExchange exchange) throws IOException {
            exchange.sendResponseHeaders(HTTP_OK, bytesToSend.length);                                     OutputStream responseBody = exchange.getResponseBody();
            responseBody.write(bytesToSend);
            responseBody.close();
     } 

Кстати, и этот код, и ваш код используют getBytes(). Это означает, что он будет использовать кодировку платформы по умолчанию, что не является хорошей практикой. Лучше назвать его с явной кодировкой, например getBytes("UTF-8")

Еще одно замечание: я исправил ваш код, предполагая, что это настоящий код. Если ваша логика более сложна, например, Вы разрешаете скачивать несколько файлов, лучше использовать потоковую передачу: читать входной файл по чанкам и отправлять чанки по запросу. Не храните слишком много фрагментов в памяти.

...