これは Java に対する長年の不満でしたが、ほとんど意味がなく、通常は間違った情報に基づいています。通常の言い回しは、「Java の Hello World は 10 メガバイトかかります! なぜそれが必要なのですか?」のようなものです。さて、これは 64 ビット JVM で Hello World が 4 ギガバイトを超えると主張する方法です ... 少なくとも 1 つの測定形式によって。
java -Xms1024m -Xmx4096m com.example.Hello
メモリを測定するさまざまな方法
Linux では、top コマンドを実行すると、メモリのいくつかの異なる数値が得られます。 Hello World の例については、次のように説明されています。
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2120 kgregory 20 0 4373m 15m 7152 S 0 0.2 0:00.10 java
- VIRT は仮想メモリ空間です。仮想メモリ マップ内のすべてのものの合計です (以下を参照)。そうでない場合を除いて、ほとんど意味がありません (下記参照)。
- RES は常駐セット サイズです。現在 RAM に常駐しているページの数です。ほとんどの場合、これは「大きすぎる」と言うときに使用する唯一の数値です。しかし、特に Java について話すときは、まだあまり良い数字ではありません。
- SHR は、他のプロセスと共有される常駐メモリの量です。 Java プロセスの場合、これは通常、共有ライブラリとメモリ マップされた JAR ファイルに限定されます。この例では、1 つの Java プロセスしか実行していないため、7k は OS で使用されるライブラリの結果であると思われます。
- SWAP はデフォルトではオンになっていないため、ここには表示されません。 実際にスワップ空間にあるかどうか、現在ディスクに常駐している仮想メモリの量を示します . OS はアクティブなページを RAM に保持することに非常に優れており、スワッピングの唯一の解決策は (1) メモリを追加購入するか、(2) プロセス数を減らすことです。そのため、この数を無視することをお勧めします。
Windows タスク マネージャーの状況はもう少し複雑です。 Windows XP では、"メモリ使用量" と "仮想メモリ サイズ" の列がありますが、公式ドキュメントではそれらが何を意味するかについては言及されていません。 Windows Vista と Windows 7 ではさらに列が追加されており、実際に文書化されています。これらのうち、「ワーキング セット」測定が最も有用です。 Linux の RES と SHR の合計にほぼ相当します。
仮想メモリ マップについて
プロセスによって消費される仮想メモリは、プロセス メモリ マップにあるすべてのものの合計です。これには、データ (Java ヒープなど) だけでなく、プログラムで使用されるすべての共有ライブラリとメモリ マップ ファイルも含まれます。 Linux では、pmap コマンドを使用して、プロセス空間にマッピングされたすべてのものを表示できます (ここからは Linux についてのみ言及します。これは私が使用しているものなので、Linux に相当するツールがあると確信しています。ウィンドウズ)。これは、「Hello World」プログラムのメモリ マップからの抜粋です。メモリ マップ全体の長さは 100 行を超えており、1,000 行のリストを持つことも珍しくありません。
0000000040000000 36K r-x-- /usr/local/java/jdk-1.6-x64/bin/java 0000000040108000 8K rwx-- /usr/local/java/jdk-1.6-x64/bin/java 0000000040eba000 676K rwx-- [ anon ] 00000006fae00000 21248K rwx-- [ anon ] 00000006fc2c0000 62720K rwx-- [ anon ] 0000000700000000 699072K rwx-- [ anon ] 000000072aab0000 2097152K rwx-- [ anon ] 00000007aaab0000 349504K rwx-- [ anon ] 00000007c0000000 1048576K rwx-- [ anon ] ... 00007fa1ed00d000 1652K r-xs- /usr/local/java/jdk-1.6-x64/jre/lib/rt.jar ... 00007fa1ed1d3000 1024K rwx-- [ anon ] 00007fa1ed2d3000 4K ----- [ anon ] 00007fa1ed2d4000 1024K rwx-- [ anon ] 00007fa1ed3d4000 4K ----- [ anon ] ... 00007fa1f20d3000 164K r-x-- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so 00007fa1f20fc000 1020K ----- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so 00007fa1f21fb000 28K rwx-- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so ... 00007fa1f34aa000 1576K r-x-- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3634000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3833000 16K r-x-- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3837000 4K rwx-- /lib/x86_64-linux-gnu/libc-2.13.so ...
フォーマットの簡単な説明:各行は、セグメントの仮想メモリ アドレスで始まります。これに続いて、セグメント サイズ、権限、およびセグメントのソースが続きます。この最後の項目は、ファイルまたは「anon」のいずれかで、mmap によって割り当てられたメモリのブロックを示します。
上から順に
- JVM ローダー (つまり、
java
と入力すると実行されるプログラム) )。これは非常に小さいです。実際の JVM コードが保存されている共有ライブラリにロードするだけです。 - Java ヒープと内部データを保持する一連の anon ブロック。これは Sun JVM であるため、ヒープは複数の世代に分割され、それぞれが独自のメモリ ブロックになります。 JVM は
-Xmx
に基づいて仮想メモリ空間を割り当てることに注意してください。 価値;これにより、連続したヒープを持つことができます。-Xms
value は、プログラムの開始時に「使用中」のヒープの量を示し、その制限に近づくとガベージ コレクションをトリガーするために内部的に使用されます。 - メモリ マップされた JAR ファイル。この場合は、「JDK クラス」を保持するファイルです。 JAR をメモリ マップすると、JAR 内のファイルに非常に効率的にアクセスできます (毎回最初から読み取るのではなく)。 Sun JVM は、クラスパス上のすべての JAR をメモリ マップします。アプリケーション コードが JAR にアクセスする必要がある場合は、JAR をメモリ マップすることもできます。
- 2 つのスレッドのスレッドごとのデータ。 1M ブロックはスレッド スタックです。私は 4k ブロックについて適切な説明を持っていませんでしたが、@ericsoe はそれを「ガード ブロック」として識別しました。読み取り/書き込み権限がないため、アクセスするとセグメント フォールトが発生し、JVM がそれをキャッチして変換します
StackOverFlowError
に .実際のアプリの場合、これらのエントリがメモリ マップを通じて繰り返される数百とまではいかなくても、数十が表示されます。 - 実際の JVM コードを保持する共有ライブラリの 1 つ。いくつかあります。
- C 標準ライブラリの共有ライブラリ。これは、厳密には Java の一部ではない、JVM がロードする多くのものの 1 つにすぎません。
共有ライブラリは特に興味深いものです。各共有ライブラリには少なくとも 2 つのセグメントがあります。ライブラリ コードを含む読み取り専用セグメントと、ライブラリのプロセスごとのグローバル データを含む読み書き可能セグメントです (パーミッションのないセグメントは、x64 Linux でしか見たことがありません)。ライブラリの読み取り専用部分は、ライブラリを使用するすべてのプロセス間で共有できます。例:libc
共有可能な 1.5M の仮想メモリ空間があります。
仮想メモリ サイズが重要な場合
仮想メモリマップには多くのものが含まれています。一部は読み取り専用で、一部は共有され、一部は割り当てられているが決して操作されません (たとえば、この例では 4Gb のヒープのほぼすべて)。しかし、オペレーティング システムは必要なものだけをロードするほどスマートなので、仮想メモリのサイズはほとんど関係ありません。
仮想メモリ サイズが重要になるのは、32 ビット オペレーティング システムで実行している場合で、2Gb (場合によっては 3Gb) のプロセス アドレス空間しか割り当てることができません。その場合、希少なリソースを扱っているため、大きなファイルをメモリ マップしたり、多数のスレッドを作成したりするためにヒープ サイズを縮小するなどのトレードオフが必要になる場合があります。
しかし、64 ビット マシンがどこにでもあることを考えると、仮想メモリ サイズが完全に無関係な統計になるまでそう長くはかからないと思います。
常駐セットのサイズが重要なのはいつですか?
常駐セットのサイズは、実際に RAM にある仮想メモリ空間の部分です。 RSS が物理メモリ全体のかなりの部分を占めるようになったら、心配し始める時期かもしれません。 RSS がすべての物理メモリを占有するようになり、システムがスワッピングを開始した場合は、心配する必要はありません。
しかし、特に負荷の軽いマシンでは、RSS も誤解を招きます。オペレーティング システムは、プロセスによって使用されるページを再利用するために多くの労力を費やすことはありません。そうすることで得られるメリットはほとんどなく、プロセスが将来ページにアクセスした場合に、コストのかかるページ フォールトが発生する可能性があります。その結果、RSS 統計には、アクティブに使用されていないページが多数含まれる場合があります。
結論
スワッピングをしている場合を除き、さまざまなメモリ統計が何を示しているかについて過度に心配する必要はありません。増え続ける RSS は、ある種のメモリ リークを示している可能性があることに注意してください。
Java プログラムでは、ヒープで何が起こっているかに注意を払うことがはるかに重要です。消費されるスペースの総量は重要であり、それを削減するために実行できるいくつかの手順があります。さらに重要なのは、ガベージ コレクションに費やす時間と、ヒープのどの部分が収集されているかです。
ディスク (データベースなど) へのアクセスは高価ですが、メモリは安価です。一方を他方と交換できる場合は、そうしてください。
Java プロセスに割り当てられたメモリの量は、私が予想していたものとほとんど同じです。組み込み/メモリが制限されたシステムでJavaを実行すると、同様の問題が発生しました。 任意のを実行 任意の VM 制限があるアプリケーションや、適切な量のスワップがないシステムでは、アプリケーションが壊れる傾向があります。これは、リソースが限られたシステムで使用するように設計されていない多くの最新のアプリの性質のようです.
JVM のメモリ フットプリントを試して制限できるいくつかのオプションがあります。これにより、仮想メモリのフットプリントが削減される可能性があります:
<ブロック引用>-XX:ReservedCodeCacheSize=32m 予約済みコード キャッシュ サイズ (バイト単位) - 最大コード キャッシュ サイズ。 [Solaris 64 ビット、amd64、および -server x86:48m; in1.5.0_06 以前、Solaris 64 ビットおよび and64:1024m.]
-XX:MaxPermSize=64m 永久世代のサイズ。 [5.0 以降:64 ビット VM は 30% 大きくスケーリングされます。 1.4amd64:96m; 1.3.1 - クライアント:32m.]
また、-Xmx (最大ヒープ サイズ) を、実際のピーク メモリ使用量にできるだけ近い値に設定する必要があります。 あなたのアプリケーションの。 JVM のデフォルトの動作はまだ 2 倍 だと思います 最大まで拡張するたびにヒープサイズ。 32M ヒープで開始し、アプリが 65M に達した場合、ヒープは最終的に 32M -> 64M -> 128M に増加します。
また、VM がヒープの拡大を積極的に行わないようにするために、次の方法を試すこともできます。
<ブロック引用>-XX:MinHeapFreeRatio=40 拡張を避けるための GC 後のヒープの最小パーセンテージ。
また、数年前にこれを試したときのことを思い出すと、読み込まれたネイティブ ライブラリの数が最小フットプリントに大きな影響を与えました。私の記憶が正しければ、java.net.Socket をロードすると 15M 以上追加されました (おそらく覚えていません)。
Java と glibc>=2.10 (Ubuntu>=10.04、RHEL>=6 を含む) には既知の問題があります。
解決策は、この環境を設定することです。変数:
export MALLOC_ARENA_MAX=4
Tomcat を実行している場合は、これを TOMCAT_HOME/bin/setenv.sh
に追加できます。 ファイル。
Docker の場合、これを Dockerfile に追加します
ENV MALLOC_ARENA_MAX=4
MALLOC_ARENA_MAX の設定に関する IBM の記事がありますhttps://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=en
このブログ投稿は
<ブロック引用>常駐メモリは、メモリ リークやメモリの断片化と同様の方法でクリープすることが知られています。
未解決の JDK バグ JDK-8193521 もあります。「glibc はデフォルト設定でメモリを浪費します」
その他の参照については、Google または SO で MALLOC_ARENA_MAX を検索してください。
割り当てられたメモリの断片化が少なくなるように最適化するために、他の malloc オプションも調整することをお勧めします。
# tune glibc memory allocation, optimize for low fragmentation
# limit the number of arenas
export MALLOC_ARENA_MAX=2
# disable dynamic mmap threshold, see M_MMAP_THRESHOLD in "man mallopt"
export MALLOC_MMAP_THRESHOLD_=131072
export MALLOC_TRIM_THRESHOLD_=131072
export MALLOC_TOP_PAD_=131072
export MALLOC_MMAP_MAX_=65536