ヒープサイズよりもはるかに多くのメモリを使用しているJava(または正しいサイズのDockerメモリ制限)


118

私のアプリケーションでは、Javaプロセスで使用されるメモリはヒープサイズよりもはるかに多くなっています。

コンテナーがヒープサイズよりも多くのメモリを使用しているため、コンテナーが実行されているシステムでメモリの問題が発生し始めます。

ヒープサイズは128 MB(-Xmx128m -Xms128m)に設定され、コンテナは最大1 GBのメモリを使用します。通常の状態では、500MB必要です。Dockerコンテナの制限が(たとえばmem_limit=mem_limit=400MB)未満の場合、プロセスはOSのメモリ不足キラーによって強制終了されます。

Javaプロセスがヒープよりもはるかに多くのメモリを使用する理由を説明できますか?Dockerのメモリ制限のサイズを正しく設定するにはどうすればよいですか?Javaプロセスのオフヒープメモリフットプリントを削減する方法はありますか?


JVMのネイティブメモリトラッキングからコマンドを使用して、問題に関する詳細を収集します。

ホストシステムから、コンテナーが使用するメモリを取得します。

$ docker stats --no-stream 9afcb62a26c8
CONTAINER ID        NAME                                                                                        CPU %               MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDS
9afcb62a26c8        xx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.0acbb46bb6fe3ae1b1c99aff3a6073bb7b7ecf85   0.93%               461MiB / 9.744GiB   4.62%               286MB / 7.92MB      157MB / 2.66GB      57

コンテナーの内部から、プロセスが使用するメモリーを取得します。

$ ps -p 71 -o pcpu,rss,size,vsize
%CPU   RSS  SIZE    VSZ
11.2 486040 580860 3814600

$ jcmd 71 VM.native_memory
71:

Native Memory Tracking:

Total: reserved=1631932KB, committed=367400KB
-                 Java Heap (reserved=131072KB, committed=131072KB)
                            (mmap: reserved=131072KB, committed=131072KB) 

-                     Class (reserved=1120142KB, committed=79830KB)
                            (classes #15267)
                            (  instance classes #14230, array classes #1037)
                            (malloc=1934KB #32977) 
                            (mmap: reserved=1118208KB, committed=77896KB) 
                            (  Metadata:   )
                            (    reserved=69632KB, committed=68272KB)
                            (    used=66725KB)
                            (    free=1547KB)
                            (    waste=0KB =0.00%)
                            (  Class space:)
                            (    reserved=1048576KB, committed=9624KB)
                            (    used=8939KB)
                            (    free=685KB)
                            (    waste=0KB =0.00%)

-                    Thread (reserved=24786KB, committed=5294KB)
                            (thread #56)
                            (stack: reserved=24500KB, committed=5008KB)
                            (malloc=198KB #293) 
                            (arena=88KB #110)

-                      Code (reserved=250635KB, committed=45907KB)
                            (malloc=2947KB #13459) 
                            (mmap: reserved=247688KB, committed=42960KB) 

-                        GC (reserved=48091KB, committed=48091KB)
                            (malloc=10439KB #18634) 
                            (mmap: reserved=37652KB, committed=37652KB) 

-                  Compiler (reserved=358KB, committed=358KB)
                            (malloc=249KB #1450) 
                            (arena=109KB #5)

-                  Internal (reserved=1165KB, committed=1165KB)
                            (malloc=1125KB #3363) 
                            (mmap: reserved=40KB, committed=40KB) 

-                     Other (reserved=16696KB, committed=16696KB)
                            (malloc=16696KB #35) 

-                    Symbol (reserved=15277KB, committed=15277KB)
                            (malloc=13543KB #180850) 
                            (arena=1734KB #1)

-    Native Memory Tracking (reserved=4436KB, committed=4436KB)
                            (malloc=378KB #5359) 
                            (tracking overhead=4058KB)

-        Shared class space (reserved=17144KB, committed=17144KB)
                            (mmap: reserved=17144KB, committed=17144KB) 

-               Arena Chunk (reserved=1850KB, committed=1850KB)
                            (malloc=1850KB) 

-                   Logging (reserved=4KB, committed=4KB)
                            (malloc=4KB #179) 

-                 Arguments (reserved=19KB, committed=19KB)
                            (malloc=19KB #512) 

-                    Module (reserved=258KB, committed=258KB)
                            (malloc=258KB #2356) 

$ cat /proc/71/smaps | grep Rss | cut -d: -f2 | tr -d " " | cut -f1 -dk | sort -n | awk '{ sum += $1 } END { print sum }'
491080

アプリケーションは、36 MBのファット内にバンドルされたJetty / Jersey / CDIを使用するWebサーバーです。

以下のバージョンのOSおよびJavaが使用されます(コンテナー内)。Dockerイメージはに基づいていopenjdk:11-jre-slimます。

$ java -version
openjdk version "11" 2018-09-25
OpenJDK Runtime Environment (build 11+28-Debian-1)
OpenJDK 64-Bit Server VM (build 11+28-Debian-1, mixed mode, sharing)
$ uname -a
Linux service1 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 GNU/Linux

https://gist.github.com/prasanthj/48e7063cac88eb396bc9961fb3149b58


6
ヒープはオブジェクトが割り当てられる場所ですが、JVMには共有ライブラリ、ダイレクトメモリバッファ、スレッドスタック、GUIコンポーネント、メタスペースなど、他の多くのメモリ領域があります。JVMの大きさを確認し、制限を十分に高くして、プロセスを使用せずに停止させる必要があります。
Peter Lawrey、

2
GCが大量のメモリを使用しているようです。代わりにCMSコレクターを使用してみてください。メタスペース+コードに約125 MBが使用されているようですが、コードベースを縮小しないと、それを小さくすることはできません。コミットされたスペースはあなたの限界に近いので、それが殺されても驚くことではありません。
Peter Lawrey、

どこで/ Xmsおよび-Xmx構成をどのように設定しますか?
Mick


1
プログラムは多くのファイル操作を実行しますか(ギガバイトサイズのファイルを作成するなど)?その場合、cgroupsディスクキャッシュが使用済みメモリに追加されることを知っておく必要があります-カーネルによって処理され、ユーザープログラムからは見えない場合でも同様です。(psdocker stats
注意して

回答:


204

Javaプロセスで使用される仮想メモリは、Javaヒープだけにとどまりません。ご存知のとおり、JVMにはガベージコレクター、クラスロード、JITコンパイラーなどの多くのサブシステムが含まれており、これらのサブシステムはすべて、機能するために一定量のRAMを必要とします。

RAMを消費するのはJVMだけではありません。ネイティブライブラリ(標準のJavaクラスライブラリを含む)もネイティブメモリを割り当てる場合があります。そして、これはNative Memory Trackingにも表示されません。Javaアプリケーション自体も、直接ByteBufferを使用してオフヒープメモリを使用できます。

それでは、Javaプロセスで何がメモリを必要としますか?

JVMパーツ(主にネイティブメモリトラッキングで示される)

  1. Javaヒープ

    最も明白な部分。これは、Javaオブジェクトが存在する場所です。ヒープは最大-Xmx量のメモリを使用します。

  2. ガベージコレクター

    GC構造とアルゴリズムは、ヒープ管理のために追加のメモリを必要とします。これらの構造は、ビットマップのマーク、スタックのマーク(オブジェクトグラフのトラバース)、記憶されたセット(領域間の参照の記録)などです。それらの一部は直接調整可能です。たとえば-XX:MarkStackSizeMax、他はヒープレイアウトに依存します。たとえば、大きい方はG1領域です(-XX:G1HeapRegionSize)ほど、記憶されるセットは小さくなります。

    GCメモリのオーバーヘッドは、GCアルゴリズムによって異なります。-XX:+UseSerialGCそして、-XX:+UseShenandoahGC最小のオーバーヘッドを持っています。G1またはCMSは、合計ヒープサイズの約10%を簡単に使用できます。

  3. コードキャッシュ

    動的に生成されたコード(JITでコンパイルされたメソッド、インタープリター、ランタイムスタブ)が含まれています。サイズには制限があります-XX:ReservedCodeCacheSize(デフォルトでは240M)。オフ-XX:-TieredCompilationにすると、コンパイルされたコードの量が減り、コードキャッシュの使用量が減ります。

  4. コンパイラ

    JITコンパイラー自体も、その作業を行うためにメモリーを必要とします。これは、階層化コンパイルをオフにするか、コンパイラスレッドの数を減らすことで、再び削減できます-XX:CICompilerCount

  5. クラスローディング

    クラスメタデータ(メソッドのバイトコード、シンボル、定数プール、注釈など)は、メタスペースと呼ばれるオフヒープ領域に格納されます。より多くのクラスがロードされます-より多くのメタスペースが使用されます。合計使用量は-XX:MaxMetaspaceSize(デフォルトでは無制限)および-XX:CompressedClassSpaceSize(デフォルトでは1G )に制限 できます。

  6. シンボルテーブル

    JVMの2つの主要なハッシュテーブル:シンボルテーブルには名前、署名、識別子などが含まれ、文字列テーブルにはインターンされた文字列への参照が含まれます。ネイティブメモリトラッキングが文字列テーブルによるメモリ使用量を示している場合は、アプリケーションが過度にを呼び出している可能性がありますString.intern

  7. スレッド

    スレッドスタックは、RAMの取得も担当します。スタックサイズはによって制御され-Xssます。デフォルトはスレッドあたり1Mですが、幸いなことにそれほど問題ではありません。OSはメモリページを遅延して、つまり最初の使用時に割り当てるため、実際のメモリ使用量ははるかに少なくなります(通常、スレッドスタックごとに80〜200 KB)。RSSのどれだけがJavaスレッドスタックに属するかを推定するスクリプトを書きました。

    ネイティブメモリを割り当てる他のJVMパーツもありますが、それらは通常、総メモリ消費量に大きな役割を果たしません。

直接バッファ

アプリケーションは、を呼び出すことにより、ヒープ外メモリを明示的に要求できますByteBuffer.allocateDirect。デフォルトのオフヒープ制限はと同じですが-Xmx、で上書きできます-XX:MaxDirectMemorySize。Direct ByteBufferはOther、NMT出力のセクションに含まれています(またはInternalJDK 11より前)。

使用されているダイレクトメモリの量は、JConsoleやJava Mission ControlなどでJMXを介して確認できます。

BufferPool MBean

直接ByteBufferの他MappedByteBuffersに、プロセスの仮想メモリにマップされたファイルが存在する可能性があります。NMTはそれらを追跡しませんが、MappedByteBuffersは物理メモリを使用することもできます。そして、彼らが取ることができる量を制限する簡単な方法はありません。プロセスのメモリマップを見れば、実際の使用状況を確認できます。pmap -x <pid>

Address           Kbytes    RSS    Dirty Mode  Mapping
...
00007f2b3e557000   39592   32956       0 r--s- some-file-17405-Index.db
00007f2b40c01000   39600   33092       0 r--s- some-file-17404-Index.db
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

ネイティブライブラリ

によってロードされたJNIコードはSystem.loadLibrary、JVM側からの制御なしで、必要なだけのオフヒープメモリを割り当てることができます。これは、標準のJavaクラスライブラリにも関係します。特に、閉じられていないJavaリソースは、ネイティブメモリリークの原因になる可能性があります。典型的な例はZipInputStreamまたはDirectoryStreamです。

特にJVMTIエージェント jdwpデバッグエージェント-は、過度のメモリ消費を引き起こす可能性もあります。

この回答は、ネイティブメモリ割り当てのプロファイルを作成する方法を説明しています async-profilerを使用プロファイリング

アロケーターの問題

プロセスは通常、OSから直接(mmapシステムコールによって)またはmalloc標準のlibcアロケーターを使用してネイティブメモリを要求します。次に、mallocを使用してOSからメモリの大きなチャンクを要求mmapし、独自の割り当てアルゴリズムに従ってこれらのチャンクを管理します。問題は、このアルゴリズムが断片化と過剰な仮想メモリの使用につながる可能性があることです。

jemalloc、代替アロケータは、通常のlibcよりもスマートに見えることが多いmallocため、に切り替えるとjemalloc、無料でフットプリントが小さくなる可能性があります。

結論

考慮すべき要素が多すぎるため、Javaプロセスの完全なメモリ使用量を見積もる方法は保証されていません。

Total memory = Heap + Code Cache + Metaspace + Symbol tables +
               Other JVM structures + Thread stacks +
               Direct buffers + Mapped files +
               Native Libraries + Malloc overhead + ...

特定のメモリ領域(コードキャッシュなど)をJVMフラグで縮小または制限することは可能ですが、他の多くの領域はJVMの制御外にあります。

Docker制限を設定する1つの可能なアプローチは、プロセスの「通常の」状態での実際のメモリ使用量を監視することです。Javaのメモリ消費に関する問題を調査するためのツールとテクニックがあります:Native Memory Trackingpmapjemallocasync-profiler

更新

これが私のプレゼンテーションのJavaプロセスのメモリフットプリントです

このビデオでは、Javaプロセスでメモリを消費する可能性のあるもの、特定のメモリ領域のサイズを監視および制限する方法、Javaアプリケーションでネイティブメモリリークをプロファイリングする方法について説明します。


1
jdk7以降、ヒープにインターンされた文字列はありませんか?(bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931)-多分私は間違っています。
j-keck

5
@ j-keck Stringオブジェクトはヒープ内にありますが、ハッシュテーブル(バケットと参照とハッシュコードを含むエントリ)はヒープ外メモリにあります。文章をより正確に言い換えました。ご指摘ありがとうございます。
アパンギン2018

これに追加するには、非ダイレクトByteBufferを使用していても、JVMは一時的なダイレクトバッファーをメモリ制限のないネイティブメモリーに割り当てます。Cf. evanjones.ca/java-bytebuffer-leak.html
Cpt。Senkfuss

16

https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/

-Xmx = 1gを指定すると、JVMが1GBのメモリより多くのメモリを消費するのはなぜですか?

-Xmx = 1gを指定すると、JVMに1GBのヒープを割り当てるように指示されます。メモリ全体の使用量を1GBに制限するようにJVMに指示しているわけではありません。カードテーブル、コードキャッシュ、および他のあらゆる種類のヒープ外データ構造があります。総メモリ使用量を指定するために使用するパラメーターは-XX:MaxRAMです。-XX:MaxRam = 500mの場合、ヒープは約250MBになることに注意してください。

Javaはホストのメモリサイズを認識し、コンテナのメモリ制限を認識しません。メモリの圧迫は発生しないため、GCは使用済みメモリを解放する必要もありません。私XX:MaxRAMはあなたがメモリのフットプリントを減らすのに役立つことを願っています。最終的に、あなたはGCの設定を微調整することができます(-XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio、...)


メモリメトリックには多くの種類があります。DockerがRSSメモリサイズを報告しているようです。これは、jcmd(古いバージョンのDockerがRSS +キャッシュをメモリ使用量として報告する)によって報告された「コミットされた」メモリとは異なる場合があります。良い議論とリンク:Dockerコンテナーで実行されているJVMの常駐セットサイズ(RSS)とJava合計コミットメモリ(NMT)の違い

(RSS)メモリーは、コンテナー内の他のいくつかのユーティリティ(シェル、プロセスマネージャーなど)によっても消費される可能性があります。コンテナー内で他に何が実行されているか、およびコンテナー内でプロセスを開始する方法はわかりません。


それは確かにでより良い-XX:MaxRamです。定義された最大値を超えてまだ使用していると思いますが、より良いです、ありがとう!
Nicolas Henneaux 2018

たぶん、あなたは本当にこのJavaインスタンスのためにより多くのメモリを必要としています。15267クラス、56スレッドがあります。
Jan Garaj 2018

1
詳細は、Java引数-Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC、生成Docker 428.5MiB / 600MiB、およびjcmd 58 VM.native_memory -> Native Memory Tracking: Total: reserved=1571296KB, committed=314316KBです。コンテナーが430MBを必要とする一方で、JVMは約300MBを使用しています。JVMレポートとOSレポートの間の130MBはどこですか?
Nicolas Henneaux 2018

1
RSSメモリに関する情報/リンクを追加しました。
Jan Garaj

提供されるRSSは、Javaプロセスのコンテナー内からのもので、ps -p 71 -o pcpu,rss,size,vsizepid 71 のJavaプロセスのみです。実際に-XX:MaxRamは役に立ちませんでしたが、提供したリンクはシリアルGCに役立ちます。
ニコラ・エノー

8

TL; DR

メモリの詳細な使用法は、Native Memory Tracking(NMT)の詳細(主にコードメタデータとガベージコレクター)によって提供されます。それに加えて、JavaコンパイラとオプティマイザC1 / C2は、要約で報告されていないメモリを消費します。

メモリフットプリントは、JVMフラグを使用して削減できます(ただし、影響があります)。

Dockerコンテナーのサイジングは、アプリケーションの予想される負荷でのテストを通じて行う必要があります。


各コンポーネントの詳細

共有クラス・スペースは、クラスが別のJVMプロセスで共有されることはありませんので、コンテナ内で無効にすることができます。次のフラグを使用できます。共有クラススペース(17MB)が削除されます。

-Xshare:off

ガベージコレクタシリアルは、(参照ガベージコレクト処理中のより長い休止時間のコストで最小のメモリフットプリントを有し、一の画面にGC間アレクセイShipilëv比較します)。次のフラグで有効にできます。使用するGCスペース(48MB)まで節約できます。

-XX:+UseSerialGC

C2コンパイラは、メソッド最適化するか否かを決定するために使用されるプロファイルデータを削減するために、次のフラグを使用して無効にすることができます。

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

コードスペースが20MB削減されます。さらに、JVM外のメモリは80MB削減されます(NMTスペースとRSSスペースの違い)。最適化コンパイラC2には100MBが必要です。

C1とC2コンパイラ、次のフラグで無効にすることができます。

-Xint

現在、JVMの外部のメモリは、コミットされた合計容量よりも少なくなっています。コードスペースは43MB削減されます。これは、アプリケーションのパフォーマンスに大きな影響を与えることに注意してください。C1およびC2コンパイラを無効にすると、使用されるメモリが170 MB削減されます。

Graal VMコンパイラの使用(C2の置き換え)すると、メモリフットプリントが少し小さくなります。コードメモリ空間が20MB増加し、JVMメモリの外部から60MB減少します。

記事「JVMのJavaメモリ管理では、さまざまなメモリ空間に関するいくつかの関連情報を提供しています。Oracleは、Native Memory Trackingのドキュメントで詳細を提供しています高度なコンパイルポリシーC2無効にした場合のコンパイルレベルの詳細は、コードキャッシュサイズを5倍に減らしますJVMがLinuxプロセスの常駐セットサイズよりもコミットされたメモリを報告する理由の詳細 両方のコンパイラが無効になっている場合。


-1

Javaには大量のメモリが必要です。JVM自体を実行するには、大量のメモリが必要です。ヒープは、仮想マシン内で利用可能なメモリであり、アプリケーションで利用できます。JVMは可能な限りの機能が満載の大きなバンドルであるため、ロードするだけで大​​量のメモリを消費します。

Java 9以降、プロジェクトジグソーと呼ばれるものがあります。以降では、。これにより、Javaアプリの起動時に使用されるメモリが(開始時間とともに)削減される可能性があります。プロジェクトのジグソーパズルと新しいモジュールシステムは、必要なメモリを減らすために必ずしも作成されたわけではありませんが、それが重要な場合は、試してみることができます。

この例をご覧くださいhttps : //steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/。モジュールシステムを使用することで、21 MBのCLIアプリケーションが作成されました(JREが組み込まれています)。JREは200MB以上かかります。これは、アプリケーションが稼働しているときに割り当てられるメモリが少なくなることを意味します(多くの未使用のJREクラスはロードされなくなります)。

ここに別の素晴らしいチュートリアルがあります:https : //www.baeldung.com/project-jigsaw-java-modularity

これに時間をかけたくない場合は、より多くのメモリを割り当てることができます。時にはそれは最高です。


jlinkアプリケーションをモジュール化する必要があるため、使用にはかなり制限があります。自動モジュールはサポートされていないため、簡単にアクセスする方法はありません。
Nicolas Henneaux 2018

-1

Dockerのメモリ制限のサイズを正しく設定するにはどうすればよいですか? しばらく監視して、アプリケーションを確認します。コンテナーのメモリを制限するには、docker runコマンドの-m、-memory bytesオプションを使用してみてください。

docker run -d --name my-container --memory 500m <iamge-name>

他の質問には答えられません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.