1. ホーム
  2. java

[解決済み] Javaがヒープサイズ(またはDockerのメモリ制限を満たすサイズ)よりはるかに多くのメモリを使用する。

2022-05-06 12:19:53

質問

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

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

ヒープサイズは128MBに設定されています( -Xmx128m -Xms128m )、コンテナは最大1GBのメモリを使用します。通常の場合、500MB必要です。もし、ドッカーコンテナに以下のような制限がある場合(例えば 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を使用したWebサーバーで、36MBのファットファー内にバンドルされています。

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

解決方法は?

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

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

では、Javaのプロセスでは、何がメモリを消費するのでしょうか。

JVM部分(主にNative Memory Trackingで表示されます。)

  1. Javaヒープ

一番わかりやすいところ。Javaオブジェクトが住んでいる場所です。ヒープには、最大で -Xmx のメモリ量になります。

  1. ガーベッジコレクタ

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

GCメモリのオーバーヘッドは、GCアルゴリズムによって異なる。 -XX:+UseSerialGC-XX:+UseShenandoahGC は、オーバーヘッドが最も小さい。G1やCMSはヒープサイズの10%程度を簡単に使用することができます。

  1. コードキャッシュ

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

  1. コンパイラ

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

  1. クラスの読み込み

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

  1. シンボルテーブル

JVMの2つの主要なハッシュテーブル:シンボル・テーブルは、名前、署名、識別子などを含み、ストリング・テーブルは、内部文字列への参照を含む。Native Memory TrackingがStringテーブルによる大幅なメモリ使用を示す場合、それはおそらく、アプリケーションが過度に String.intern .

  1. スレッド

スレッドスタックはRAMを占有する役割も担っています。スタックの大きさは -Xss . デフォルトは1スレッドあたり1Mですが、幸いなことにそれほど悪い状況にはなっていません。OSはメモリページを遅延的に、つまり最初に使用するときに割り当てますので、実際のメモリ使用量はもっと少なくなります(通常、スレッドスタックあたり80-200KB)。私は スクリプト を使用して、RSSがJavaスレッドスタックにどれだけ属しているかを推定しています。

他にもネイティブ・メモリを確保するJVMのパーツはありますが、通常、総メモリ消費量に大きな役割を果たすことはありません。

ダイレクトバッファ

アプリケーションは、明示的にオフヒープメモリを要求するために ByteBuffer.allocateDirect . デフォルトのオフヒープの上限は -Xmx で上書きすることができます。 -XX:MaxDirectMemorySize . Direct ByteBuffer は Other セクションを作成します(または Internal JDK 11以前)。

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

直接のByteBufferの他に、以下のようなものがあります。 MappedByteBuffers - プロセスの仮想メモリにマッピングされたファイルです。NMTはこれらを追跡しませんが、MappedByteBufferは物理メモリを占有することもできます。そして、それらが取ることができる量を制限する簡単な方法はありません。プロセスのメモリマップを見れば、実際の使用量を確認できます。 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 デバッギング・エージェントもまた、過剰なメモリ消費を引き起こす可能性があります。

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

アロケーターの問題

プロセスは通常、ネイティブメモリを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つの可能なアプローチは、プロセスの"normal"状態での実際のメモリ使用量を監視することです。Javaメモリ消費に関する問題を調査するためのツールやテクニックがあります。 ネイティブメモリトラッキング , pmap , ジェマロク , 非同期プロファイラ .

更新情報

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

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