1. ホーム
  2. java

[解決済み] ダイレクトバッファメモリ

2022-02-18 22:40:23

質問事項

ウェブリクエストからかなり大きなファイルを返す必要があります。ファイルの大きさは約670mbです。ほとんどの場合、これは問題なく動作しますが、しばらくすると次のエラーがスローされます。

java.lang.OutOfMemoryError: Direct buffer memory
    at java.nio.Bits.reserveMemory(Bits.java:694) ~[na:1.8.0_162]
    at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123) ~[na:1.8.0_162]
    at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) ~[na:1.8.0_162]
    at sun.nio.ch.Util.getTemporaryDirectBuffer(Util.java:241) ~[na:1.8.0_162]
    at sun.nio.ch.IOUtil.read(IOUtil.java:195) ~[na:1.8.0_162]
    at sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:159) ~[na:1.8.0_162]
    at sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:65) ~[na:1.8.0_162]
    at sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:109) ~[na:1.8.0_162]
    at sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:103) ~[na:1.8.0_162]
    at java.nio.file.Files.read(Files.java:3105) ~[na:1.8.0_162]
    at java.nio.file.Files.readAllBytes(Files.java:3158) ~[na:1.8.0_162]

ヒープサイズを4096mbに設定しましたが、これはこの種のファイルを扱うのに十分な大きさだと思われます。さらに、このエラーが発生したとき、私は現在の状態を分析するためにjmapでヒープダンプを取りました。その結果、2つのかなり大きなbyte[]が見つかりました。しかし、ヒープのサイズは1.6gb程度しかなく、設定されている4gbには程遠いのです。

他の方の回答によると( https://stackoverflow.com/a/39984276/5126654 )同様の質問で、私はこのファイルを返す前に手動gcを実行してみました。問題はまだ起こりますが、今はほんの少しです。問題はしばらくして発生しましたが、その後、同じリクエストを再び実行するのに疲れたとき、ガベージコレクションが問題の原因となっていたものを処理したように思えますが、問題は明らかにまだ発生することがあるので、これは十分ではありません。このメモリの問題を回避するための他の方法はありますか?

解決方法を教えてください。

で管理している実際のメモリバッファは DirectByteBuffer はヒープに割り当てられません。 Unsafe.allocateMemoryを使用して割り当てられ、それは"ネイティブメモリ"を割り当てます。そのため、ヒープサイズを増やしても減らしても効果はありません。

を検出した場合、GCは DirectByteBuffer が参照されなくなると Cleaner はネイティブメモリを解放するために使用されます。 ただし、これはコレクション後のフェーズで行われるため、ダイレクトバッファの需要/回転が大きすぎると、コレクターが追いつかなくなる可能性があります。 その場合、OOMEが発生します。


どうしたらいいのでしょうか?

AFAIKは、あなたができる唯一のことは、より頻繁なガベージコレクションを強制することです。 しかし、それはパフォーマンスに影響を与える可能性があります。 また、保証された解決策とは思えません。

本当の解決策は、別のアプローチをとることです。

ウェブサーバーから非常に大きなファイルを大量に提供していることがわかり、スタックトレースで Files::readAllBytes を使用してメモリにロードし、(おそらく)1つの write . おそらく、可能な限り最速のダウンロード時間を得るためにこうしているのでしょう。 これは間違いです。

  • あなたは多くのメモリ(の倍数)を縛り、ガベージコレクタにストレスを与えています。 これは、より多くのGC実行と時折のOOMEにつながっています。 また、サーバー上の他のアプリケーションに様々な影響を与える可能性があります。

  • ファイル転送のボトルネックは おそらく ディスクからデータを読み込む処理ではなく (本当のボトルネックは 通常 ネットワーク上のTCPストリームでデータを送信したり、クライアント側でファイルシステムに書き込んだりします)。

  • 大きなファイルを順次読み込む場合、最近のLinux OSは通常、ディスクのブロック数を先読みして、ブロックを(OSの)バッファ・キャッシュに保持する方法を用います。 これによって read を呼び出すことができます。

ですから、このサイズのファイルでは、ファイルをストリーミングするのがベターなアイデアです。 大きな(数メガバイトの) ByteBuffer で、ループで読み書きを行う。 または を使用してファイルをコピーします。 Files::copy(...) ( ジャバドック を使用すると、バッファリングがうまくいくはずです。

(また、Linux の sendfile のシステムコールです。 これは、ユーザースペースバッファに書き込むことなく、あるファイル記述子から別のファイル記述子へデータをコピーするものです)。