OutOfMemoryError при загрузке больших файлов с помощью JDK HttpClient

Я получил OutOfMemoryError, когда попытался загрузить большой файл (> 10 ГБ) с помощью JDK HttpClient. Это произошло только тогда, когда я использовал BodyHandlers.ofInputStream. Если я использую BodyHandlers.ofFile, ошибок не было. Я использую JDK 11. Кто-нибудь знает, как это решить? Мне нужен InputStream для работы.

Выбрасывается OutOfMemoryError:

HttpResponse<InputStream> httpResponse = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
try (InputStream responseStream = httpResponse.body()) {
    Files.copy(responseStream, outputFile);
}

Ошибка не возникает:

HttpResponse<Path> httpResponse = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(outputFile));

Вот трассировка стека.

java.io.IOException: closed
    at jdk.internal.net.http.ResponseSubscribers$HttpResponseInputStream.current(ResponseSubscribers.java:342) ~[java.net.http:?]
    at jdk.internal.net.http.ResponseSubscribers$HttpResponseInputStream.read(ResponseSubscribers.java:393) ~[java.net.http:?]
    at java.io.InputStream.transferTo(InputStream.java:704) ~[?:?]
    at java.nio.file.Files.copy(Files.java:3078) ~[?:?]
    at <Masked for privacy>
    at <Masked for privacy>
    at <Masked for privacy>
Caused by: java.lang.OutOfMemoryError: Java heap space
    at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:937) ~[?:?]
    at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:491) ~[?:?]
    at javax.crypto.CipherSpi.bufferCrypt(CipherSpi.java:779) ~[?:?]
    at javax.crypto.CipherSpi.engineDoFinal(CipherSpi.java:730) ~[?:?]
    at javax.crypto.Cipher.doFinal(Cipher.java:2497) ~[?:?]
    at sun.security.ssl.SSLCipher$T12GcmReadCipherGenerator$GcmReadCipher.decrypt(SSLCipher.java:1629) ~[?:?]
    at sun.security.ssl.SSLEngineInputRecord.decodeInputRecord(SSLEngineInputRecord.java:240) ~[?:?]
    at sun.security.ssl.SSLEngineInputRecord.decode(SSLEngineInputRecord.java:197) ~[?:?]
    at sun.security.ssl.SSLEngineInputRecord.decode(SSLEngineInputRecord.java:160) ~[?:?]
    at sun.security.ssl.SSLTransport.decode(SSLTransport.java:110) ~[?:?]
    at sun.security.ssl.SSLEngineImpl.decode(SSLEngineImpl.java:681) ~[?:?]
    at sun.security.ssl.SSLEngineImpl.readRecord(SSLEngineImpl.java:636) ~[?:?]
    at sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:454) ~[?:?]
    at sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:433) ~[?:?]
    at javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:637) ~[?:?]
    at jdk.internal.net.http.common.SSLFlowDelegate$Reader.unwrapBuffer(SSLFlowDelegate.java:481) ~[java.net.http:?]
    at jdk.internal.net.http.common.SSLFlowDelegate$Reader.processData(SSLFlowDelegate.java:392) ~[java.net.http:?]
    at jdk.internal.net.http.common.SSLFlowDelegate$Reader$ReaderDownstreamPusher.run(SSLFlowDelegate.java:264) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$SynchronizedRestartableTask.run(SequentialScheduler.java:175) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:147) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:198) ~[java.net.http:?]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[?:?]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[?:?]
    at java.lang.Thread.run(Thread.java:834) ~[?:?]

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

pratap 31.03.2021 12:32

Что вам рассказала свалка кучи? (Насколько велика была куча? Что потребляло память?)

meriton 31.03.2021 12:33

Мне пришлось бы предположить, поскольку я не знаю подробностей этих классов, но я предполагаю, что обработчик, возвращаемый ofFile(), записывает данные на диск (согласно JavaDoc), а обработчик ofInputStream() хранит данные в памяти. Если так, то это объяснит вам OOM. Как предлагает pratap, сначала запишите данные в файл, а затем прочтите их оттуда - либо по частям, либо с помощью файлов с отображением в память.

Thomas 31.03.2021 12:46

@meriton Files.copy использует только 8 КБ в качестве буфера, поэтому он должен быть где-то еще для кэширования данных. Думаю, это должно быть связано с крипто-классами.

franziga 31.03.2021 12:55

@pratap Это последнее средство, если я не могу его решить.

franziga 31.03.2021 12:56

BodyHandler, возвращаемый ofInputStream(), будет хранить некоторые данные в памяти, но не должен хранить все 10 ГБ ... Какую версию (java --version) JDK 11 вы используете? Проблема воспроизводится в более поздней версии (JDK 16)?

daniel 31.03.2021 13:19

Также знаете ли вы, использовались ли HTTP / 1.1 или HTTP / 2?

daniel 31.03.2021 13:26

@daniel Я пробовал использовать Java 16, и ошибки не было. Он использует HTTP / 1.1. В любом случае я сначала сохраню весь файл на диск с помощью ofFile. Но я думаю, что fromSubscriber(Flow.Subscriber<? super List<ByteBuffer>>) может решить проблему, потому что ofFile его использует. Однако сейчас у меня нет времени, чтобы попробовать это.

franziga 02.04.2021 05:43

Я ошибался. Та же проблема возникает в Java 16.

franziga 02.04.2021 10:20

Это странно. Было бы полезно получить дамп кучи и посмотреть, какие ресурсы буферизуются и откуда.

daniel 02.04.2021 17:57
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
10
47
0

Другие вопросы по теме