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

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

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

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

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

ベストアンサー1

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

RAM を消費するのは JVM だけではありません。ネイティブ ライブラリ (標準 Java クラス ライブラリを含む) もネイティブ メモリを割り当てることがあります。これはネイティブ メモリ トラッキングでは認識されません。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 コンパイルされたメソッド、インタープリタ、およびランタイム スタブ) が含まれます。サイズは制限されています(デフォルトでは 240 MB)。コンパイルされたコードの量を減らし、コード キャッシュの使用量を減らすには、-XX:ReservedCodeCacheSizeオフにします。-XX:-TieredCompilation

4. コンパイラ

JIT コンパイラ自体も、その処理を実行するためにメモリを必要とします。このメモリは、階層型コンパイルをオフにするか、コンパイラ スレッドの数を減らすことでさらに削減できます-XX:CICompilerCount

5. クラスの読み込み

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

6. 記号表

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

7. スレッド

スレッドスタックもRAMを消費します。スタックサイズは によって制御されます-Xss。デフォルトはスレッドあたり1Mですが、幸いなことに状況はそれほど悪くありません。OSはメモリページを遅延割り当てします。つまり、最初の使用時に割り当てます。そのため、実際のメモリ使用量ははるかに低くなります(通常、スレッドスタックあたり80〜200KB)。脚本RSS のうち Java スレッド スタックに属する部分を推定します。

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

直接バッファ

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

使用中の直接メモリの量は、JConsole や Java Mission Control などの JMX を通じて確認できます。

バッファプール 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
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

ネイティブライブラリ

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

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

この答えネイティブメモリ割り当てをプロファイルする方法について説明します非同期プロファイラー

アロケータの問題

プロセスは通常、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 のメモリ消費に関する問題を調査するためのツールとテクニックがあります。ネイティブメモリトラッキングピマップジェマロック非同期プロファイラー

アップデート

私のプレゼンテーションの録画はこちらですJava プロセスのメモリ フットプリント

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

おすすめ記事