Linux で実行されている Java アプリケーションに問題があります。
アプリケーションを起動すると、デフォルトの最大ヒープ サイズ (64 MB) を使用して、tops アプリケーションを使用して、240 MB の仮想メモリがアプリケーションに割り当てられていることがわかります。これにより、比較的リソースが制限されているコンピューター上の他のソフトウェアで問題が発生します。
私の理解する限り、予約された仮想メモリは、ヒープ制限に達すると がスローされるため、いずれにしても使用されませんOutOfMemoryError
。同じアプリケーションを Windows で実行したところ、仮想メモリのサイズとヒープ サイズが似ていることがわかりました。
Linux で Java プロセスに使用する仮想メモリを構成する方法はありますか?
編集 1 : 問題はヒープではありません。問題は、たとえばヒープを 128 MB に設定しても、Linux が 210 MB の仮想メモリを割り当てることです。これはまったく必要ありません。**
編集 2 : を使用すると、ulimit -v
仮想メモリの量を制限できます。サイズ設定が 204 MB 未満の場合、必要なのは 204 MB ではなく 64 MB だけであるにもかかわらず、アプリケーションは実行されません。Java がなぜこれほど多くの仮想メモリを必要とするのか理解したいのですが。これは変更できますか?
編集 3 : システムには、組み込まれた他のアプリケーションもいくつか実行されています。また、システムには仮想メモリの制限があります (コメントから、重要な詳細)。
ベストアンサー1
これは Java に対する長年の不満ですが、大部分は無意味であり、通常は間違った情報に基づいているものです。よくある言い回しは、「Java の Hello World は 10 メガバイトもかかります。なぜそれが必要なのですか?」といったものです。さて、64 ビット JVM の Hello World が 4 ギガバイト以上を占有するようにする方法があります...少なくとも 1 つの測定方法ではそうです。
java -Xms1024m -Xmx4096m com.example.Hello
記憶力を測定するさまざまな方法
Linuxでは、上コマンドはメモリのいくつかの異なる数値を表示します。Hello World の例については次のようになります。
PID ユーザー PR NI 仮想リソース SHR S %CPU %MEM 時間+ コマンド 2120 kgregory 20 0 4373m 15m 7152 S 0 0.2 0:00.10 ジャワ
- VIRT は仮想メモリ空間です。仮想メモリ マップ内のすべての合計です (下記参照)。意味がない場合を除いて、ほとんど意味がありません (下記参照)。
- RES は常駐セット サイズ、つまり現在 RAM 内に常駐しているページ数です。ほとんどの場合、これは「大きすぎる」と判断するときに使用する唯一の数値です。しかし、特に Java について話す場合、これはまだあまり良い数値ではありません。
- SHR は、他のプロセスと共有される常駐メモリの量です。Java プロセスの場合、これは通常、共有ライブラリとメモリマップされた JAR ファイルに限定されます。この例では、実行中の Java プロセスは 1 つだけだったので、7k は OS が使用するライブラリの結果であると思われます。
- SWAP はデフォルトではオンになっていないため、ここには表示されません。これは、実際にスワップ スペースにあるかどうかに関係なく、現在ディスク上にある仮想メモリの量を示します。OS はアクティブなページを RAM に保持することに非常に優れているため、スワッピングの唯一の解決策は (1) メモリを追加するか、(2) プロセス数を減らすことです。したがって、この数値は無視するのが最善です。
Windowsタスクマネージャの状況はもう少し複雑です。Windows XPでは、「メモリ使用量」と「仮想メモリサイズ」の列がありますが、公式文書それらの意味については何も述べられていない。Windows VistaとWindows 7では列が追加され、実際には文書化されたこれらのうち、「ワーキング セット」の測定が最も有用であり、これは Linux 上の RES と SHR の合計にほぼ相当します。
仮想メモリマップを理解する
プロセスによって消費される仮想メモリは、プロセスメモリマップにあるすべてのものの合計です。これにはデータ(Javaヒープなど)だけでなく、プログラムによって使用されるすべての共有ライブラリとメモリマップファイルも含まれます。Linuxでは、ピマップコマンドを実行すると、プロセス空間にマップされているすべてのものが表示されます (ここからは、私が使用している Linux についてのみ説明します。Windows にも同等のツールがあるはずです)。以下は、「Hello World」プログラムのメモリ マップからの抜粋です。メモリ マップ全体は 100 行以上あり、1,000 行のリストになることも珍しくありません。
0000000040000000 36K rx-- /usr/local/java/jdk-1.6-x64/bin/java 0000000040108000 8K rwx-- /usr/local/java/jdk-1.6-x64/bin/java 0000000040eba000 676K rwx-- [ 匿名 ] 00000006fae00000 21248K rwx-- [ 匿名 ] 00000006fc2c0000 62720K rwx-- [ 匿名 ] 0000000700000000 699072K rwx-- [ 匿名 ] 000000072aab0000 2097152K rwx-- [ 匿名 ] 00000007aaab0000 349504K rwx-- [ 匿名 ] 00000007c0000000 1048576K rwx-- [ 匿名 ] ... 00007fa1ed00d000 1652K r-xs- /usr/local/java/jdk-1.6-x64/jre/lib/rt.jar ... 00007fa1ed1d3000 1024K rwx-- [ 匿名 ] 00007fa1ed2d3000 4K ----- [ 匿名 ] 00007fa1ed2d4000 1024K rwx-- [ 匿名 ] 00007fa1ed3d4000 4K ----- [ 匿名 ] ... 00007fa1f20d3000 164K rx-- /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 rx-- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3634000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3833000 16K rx-- /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
値は、プログラムの開始時にヒープのどれだけが「使用中」であるかを示すために内部的に使用され、その制限に近づくとガベージ コレクションがトリガーされます。 - メモリマップされた JAR ファイル。この場合は、「JDK クラス」を保持するファイルです。JAR をメモリマップすると、その中のファイルに非常に効率的にアクセスできます (毎回最初から読み取るのに比べて)。Sun JVM はクラスパス上のすべての JAR をメモリマップします。アプリケーション コードが JAR にアクセスする必要がある場合は、JAR もメモリマップできます。
- 2 つのスレッドのスレッドごとのデータ。1 MB のブロックはスレッド スタックです。4 k ブロックについてはうまく説明できませんでしたが、@ericsoe はこれを「ガード ブロック」と認識していました。読み取り/書き込み権限がないため、アクセスするとセグメント エラーが発生し、JVM がそれをキャッチして に変換します
StackOverFlowError
。実際のアプリでは、メモリ マップ全体でこのようなエントリが数十、場合によっては数百も繰り返されます。 - 実際の JVM コードを保持する共有ライブラリの 1 つ。このライブラリは複数あります。
- C 標準ライブラリの共有ライブラリ。これは、厳密には Java の一部ではないものの、JVM がロードする多くのものの 1 つにすぎません。
共有ライブラリは特に興味深いものです。各共有ライブラリには、少なくとも 2 つのセグメントがあります。ライブラリ コードを含む読み取り専用セグメントと、ライブラリのプロセスごとのグローバル データを含む読み取り/書き込みセグメントです (権限のないセグメントが何であるかはわかりません。x64 Linux でしか見たことがありません)。ライブラリの読み取り専用部分は、ライブラリを使用するすべてのプロセス間で共有できます。たとえば、libc
共有可能な仮想メモリ空間が 1.5M あります。
仮想メモリのサイズが重要になるのはいつですか?
仮想メモリ マップには多くのものが含まれています。その一部は読み取り専用、一部は共有、一部は割り当てられていて決してアクセスされないもの (この例では 4 GB のヒープのほぼすべて) です。ただし、オペレーティング システムは必要なものだけをロードするほど賢いので、仮想メモリのサイズはほとんど関係ありません。
仮想メモリのサイズが重要になるのは、32 ビット オペレーティング システムで実行している場合です。このシステムでは、2 GB (場合によっては 3 GB) のプロセス アドレス空間しか割り当てることができません。その場合、リソースが不足しており、大きなファイルをメモリ マップしたり、多数のスレッドを作成したりするためにヒープ サイズを減らすなどのトレードオフが必要になる場合があります。
しかし、64 ビット マシンが普及していることを考えると、仮想メモリ サイズがまったく無関係な統計になる日もそう遠くないと思います。
常駐セットのサイズが重要になるのはいつですか?
常駐セットのサイズは、実際に RAM 内にある仮想メモリ スペースの部分です。RSS が物理メモリ全体のかなりの部分を占めるようになったら、心配し始める時期かもしれません。RSS が物理メモリをすべて占有するほど大きくなり、システムがスワップし始めたら、心配し始める時期はとっくに過ぎています。
しかし、RSS は、特に負荷の少ないマシンでは誤解を招く恐れもあります。オペレーティング システムは、プロセスによって使用されたページを再利用するために多くの労力を費やしません。そうすることで得られるメリットはほとんどなく、プロセスが将来そのページにアクセスした場合にコストのかかるページ フォールトが発生する可能性があります。その結果、RSS 統計には、アクティブに使用されていないページが多数含まれる可能性があります。
結論
スワップしない限り、さまざまなメモリ統計が示す内容について過度に心配する必要はありません。ただし、RSS がどんどん増えていくのは、何らかのメモリ リークが発生している可能性があるという注意点があります。
Java プログラムでは、ヒープ内で何が起こっているかに注意を払うことがはるかに重要です。消費されるスペースの合計量は重要であり、それを減らすために実行できる手順がいくつかあります。さらに重要なのは、ガベージ コレクションに費やす時間の長さと、ヒープのどの部分が収集されるかです。
ディスク (つまり、データベース) へのアクセスはコストがかかりますが、メモリは安価です。どちらか一方を他方と交換できる場合は、そうしてください。