java.lang.OutOfMemoryError на HttpServer при загрузке больших данных
У меня есть 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 ответов
Не делайте этого для больших файлов:
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);
}
}
Если вы извлекаете все байты для файла одновременно, он должен прочитать их все в память, а затем записать их в файловую систему. попробуйте что-то вроде:
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();
}
Используйте потоки, чтобы вам не приходилось записывать все данные одновременно.
Смотрите getRequestBody и getResponseBody. Вы захотите открыть свой файл в виде потока и записать байты в соответствующий поток.
С такими большими объемами данных лучше всего передавать данные. Потоковая передача означает, что вы отправляете данные порциями, а не отправляете их все сразу. Это более эффективно использует память, потому что вам не нужно хранить все данные в памяти, только их части.
Кроме того, более общий способ возврата данных файла заключается в использовании обычного 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();
}
}
Проблема в вашем коде в том, что 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")
Еще одно замечание: я исправил ваш код, предполагая, что это настоящий код. В случае, если ваша логика более сложна, например, вы разрешаете скачивать несколько файлов, лучше использовать потоковую передачу: читать входной файл по частям и отправлять куски по запросу. Не храните слишком много фрагментов в памяти.
Не конвертируйте всю строку в байты сразу:
Writer writer = new OutputStreamWriter(responseBody),someEncoding);
try {
writer.write(myFile);
}
finally {
writer.close();
}