web-dev-qa-db-ja.com

ヒープサイズよりもはるかに多くのメモリを使用しているJava

私のアプリケーションでは、Javaプロセスが使用するメモリはヒープサイズよりはるかに大きいです。

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

ヒープサイズは128 MB(-Xmx128m -Xms128m)に設定されていますが、コンテナは最大1GBのメモリを消費します。通常は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

このアプリケーションは、Jetty/Jersey/CDIを使用した、36 MBの太いファットにバンドルされている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

77

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

JVMはRAMの唯一の消費者ではありません。ネイティブライブラリ(標準のJavaクラスライブラリを含む)もネイティブメモリを割り当てます。そしてこれはNative Memory Trackingからも見えません。 Javaアプリケーション自体も直接ByteBuffersを使用してオフヒープメモリを使用できます。

それでは、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コンパイラ自体もその仕事をするためにメモリを必要とします。これは、Tiered Compilationをオフにするか、コンパイラスレッドの数を減らすことで再度減らすことができます。-XX:CICompilerCount

  5. クラスローディング

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

  6. シンボルテーブル

    JVMの2つの主要なハッシュテーブル:Symbolテーブルは名前、シグニチャ、識別子などを含み、Stringテーブルは内部文字列への参照を含みます。 Native Memory TrackingがStringテーブルによる著しいメモリ使用量を示している場合、おそらくアプリケーションがString.internを過度に呼び出していることを意味します。

  7. スレッド

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

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

直接バッファー

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

使用されているダイレクトメモリの量はJMXを通して見ることができます。 JConsoleまたはJava Mission Controlの場合

BufferPool MBean

直接ByteBuffersの他に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
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

ネイティブライブラリ

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

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

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

アロケータの問題

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

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のメモリ消費に関する問題を調査するためのツールとテクニックがあります: ネイティブメモリトラッキングpmapjemallocasync-profiler

140
apangin

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

JVMが1GBを超えるメモリを消費する-Xmx = 1gを指定した場合に、どうしてですか。

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

Javaはホストのメモリサイズを認識しており、コンテナのメモリ制限を認識しません。それはメモリプレッシャーを作り出さない、従ってGCはまた使用されたメモリを解放する必要はない。 XX:MaxRAMがメモリ使用量を減らすのに役立つことを願っています。最終的には、GCの設定を微調整できます(-XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio、...)。


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

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

11
Jan Garaj

TL、DR

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

メモリー使用量はJVMフラグを使用して減らすことができます(ただし影響はあります)。

Dockerコンテナーのサイズ設定は、アプリケーションの予想される負荷でテストすることによって行う必要があります。


各コンポーネントの詳細

クラスは他のJVMプロセスと共有されないため、 共有クラススペース はコンテナ内で無効にできます。以下のフラグが使用できます。共有クラススペース(17MB)が削除されます。

-Xshare:off

ガベージコレクタ serialは、ガベージコレクション処理中の一時停止時間が長くなるという犠牲を払って、最小限のメモリ使用量で済みます( 1画像におけるGCとAlekseyShipilë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 compiler (C2の置き換え)を使用すると、メモリ使用量が少し小さくなります。コードメモリスペースが20MB増加し、外部JVMメモリから60MB減少します。

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

4

上記の答えはすべて、JVMがそれほど多くのメモリを消費する理由を示していますが、おそらく最も必要なのは解決策です。これらの記事は役に立ちます。
- https://blogs.Oracle.com/Java-platform-group/Java-se-support-for-docker-cpu-and-memory-limits
- https://royvanrijn.com/blog/2018/05/Java-and-docker-memory-limits/

0
Jason Wong