java.lang.OutOfMemoryError на HTTPServer при загрузке больших данных

#java #out-of-memory #com.sun.net.httpserver

#java #нехватка памяти #com.sun.net.httpserver

Вопрос:

У меня есть встроенный HTTPServer java 6. У него есть дескриптор, который позволяет клиентам загружать большой текстовый файл. Проблема в том, что когда на сервере одновременно работает более 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[] вместо того, чтобы создавать ее каждый раз. И я все еще получаю то же исключение.

Комментарии:

1. Пожалуйста, некоторые «кавычки кода» вокруг вашего кода..

2. @Sophie похоже, что это может быть 2 проблемы одновременно; проблема с Java и проблема с обработкой потоков. сколько подключений позволяет ваш встроенный HTTP-сервер? Какой HTTP-сервер вы используете?

3. @Sophie также, если вы установите debug=true при компиляции исходного кода, вы должны получить номера строк в трассировках стека

Ответ №1:

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

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

Комментарии:

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

2. @Sophie этот парень объясняет это нормально, просто не предоставляет деталей реализации. Решение Tarlog было просто неправильным, он считывал весь файл в другом месте (статическое поле).

3. @Sophie Не обрабатывайте байт [], содержащий все данные. Ни в какой момент вашего приложения. Вместо этого считывайте файл по частям и записывайте их «на лету» в ответ. Я рекомендую взглянуть на IOUtils .

4. @Fatal — спасибо за комментарий. У меня есть один файл, который я загружаю один раз. затем для каждого клиента я выбираю смещение от начала файла и возвращаюсь с этого момента. итак, у меня проблема с чтением «на лету», у меня всегда есть огромная строка, с которой мне нужно манипулировать.

5. @Sophie Просто пропустите смещение, а затем отправьте остальное «на лету». Опять же, я рекомендую внимательно изучить IOUtils , который предоставляет метод skip() для таких целей. В противном случае, если вы действительно не можете избежать загрузки всего файла в память, вам необходимо реализовать сервис, который ограничивает эти операции и знает об общей памяти, выделяемой такими операциями. Когда они превышают пороговое значение, эта служба должна блокировать новые запросы до тех пор, пока память не освободится от предыдущих завершенных вызовов этой службы. Но это ГОРАЗДО сложнее реализовать, особенно если вы с этим не знакомы.

Ответ №2:

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

 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();
}
 

Комментарии:

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

2. Знаете ли вы, какой наилучший размер для буфера? Я тоже всегда использую «4096», но без какой-либо конкретной причины.

3. Я всегда использую 4096 для файловых операций, потому что это размер выделения по умолчанию для многих файловых систем.

4. @Sophie также: попробуйте очистить поток после записи в него. Поэтому он не будет пытаться буферизировать ответ.

Ответ №3:

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

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

Ответ №4:

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

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

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

Использование an 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();
  }
}
 

Комментарии:

1. 1 Лучший ответ, на мой взгляд. Именно то, что я имел в виду, но с некоторым количеством кода-сахара и лучшим английским языком. 🙂

Ответ №5:

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

 Writer writer = new OutputStreamWriter(responseBody),someEncoding);
try {
  writer.write(myFile);
}
finally {
  writer.close();
}
 

Ответ №6:

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

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

       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")

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

Комментарии:

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

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

3. @MarianP — Но это работает для 5-7 клиентов с таким большим файлом. Так что не уверен, что проблема в этом файле.

4. @Sophie это может быть и их комбинация. Но правильное решение — использовать правильные потоки, как предлагают другие ребята.

5. @Sophie Возможно, что поток responseBody вывода хранит данные в своем собственном буфере, что может быть причиной того, что вы все еще получали ошибки OutOfMemory даже после использования статически определенного массива байтов.