JVMのネイティブメモリトラッキング

1. 概要

よく知られている_-Xms_および_-Xmx_チューニングフラグを介して、Javaアプリケーションが指定された量よりもはるかに多くのメモリを消費する理由を疑問に思ったことはありませんか? さまざまな理由と可能な最適化のために、JVMは追加のネイティブメモリを割り当てる場合があります。 これらの追加の割り当てにより、最終的に消費メモリが_-Xmx_制限を超える可能性があります。
このチュートリアルでは、JVMのネイティブメモリ割り当てのいくつかの一般的なソースとそのサイズ調整チューニングフラグを列挙し、_Native Memory Tracking_を使用してそれらを監視する方法を学習します。

2. ネイティブ割り当て

通常、ヒープはJavaアプリケーションのメモリの最大消費者ですが、他にもあります。 *ヒープのほかに、JVMはネイティブメモリからかなり大きなチャンクを割り当てて、クラスメタデータ、アプリケーションコード、JITによって生成されたコード、内部データ構造などを維持します。*以下のセクションでは、それらのいくつかを調べます。割り当て。

2.1. メタスペース

*ロードされたクラスに関するメタデータを維持するために、JVMは_Metaspace_ *と呼ばれる専用の非ヒープ領域を使用します。 Java 8より前は、同等のものは_PermGen_または_Permanent Generation_と呼ばれていました。 MetaspaceまたはPermGenには、ヒープ内に保持されるインスタンスではなく、ロードされたクラスに関するメタデータが含まれます。
ここで重要なことは、メタスペースはオフヒープデータ領域であるため、*ヒープサイズ設定がメタスペースサイズに影響しないことです。 メタスペースのサイズを制限するために、他のチューニングフラグを使用します。
  • _ -XX:MetaspaceSize_および_-XX:MaxMetaspaceSize_は最小値を設定し、
    最大メタスペースサイズ

  • Java 8より前では、– XX:PermSize_および-XX:MaxPermSize_で最小値を設定します
    および最大PermGenサイズ

2.2. スレッド

JVMで最もメモリを消費するデータ領域の1つは、各スレッドと同時に作成されるスタックです。 スタックはローカル変数と部分的な結果を保存し、メソッド呼び出しで重要な役割を果たします。
デフォルトのスレッドスタックサイズはプラットフォームに依存しますが、最新の64ビットオペレーティングシステムでは、約1 MBです。 このサイズは、__- Xss __tuning flagを使用して構成できます。
対照的に、他のデータ領域とは異なり、*スレッドの数に制限がない場合、スタックに割り当てられるメモリの合計は実質的に無制限です* -in-timeコンパイル。

2.3. コードキャッシュ

異なるプラットフォームでJVMバイトコードを実行するには、マシン命令に変換する必要があります。 JITコンパイラーは、プログラムの実行時にこのコンパイルを行います。
  • JVMがバイトコードをアセンブリ命令にコンパイルするとき、_Code Cacheと呼ばれる特別な非ヒープデータ領域にそれらの命令を保存します。 _ **コードキャッシュは、JVMの他のデータ領域と同様に管理できます。 – XX:InitialCodeCacheSize and — XX:ReservedCodeCacheSize tuningフラグは、コードキャッシュの初期サイズと最大可能サイズを決定します。

2.4. ガベージコレクション

JVMにはいくつかのGCアルゴリズムが同梱されており、それぞれが異なるユースケースに適しています。 これらのすべてのGCアルゴリズムは、1つの共通の特徴を共有しています。タスクを実行するには、いくつかのオフヒープデータ構造を使用する必要があります。 これらの内部データ構造は、より多くのネイティブメモリを消費します。

2.5. シンボル

アプリケーションコードとライブラリコードで最もよく使用されるデータ型の1つである__Stringsから始めましょう。 ユビキタスであるため、通常はヒープの大部分を占めます。 これらの文字列の多くに同じコンテンツが含まれている場合、ヒープのかなりの部分が無駄になります。
ヒープ領域を節約するために、各__String __の1つのバージョンを保存し、保存されたバージョンを他の人に参照させることができます。 **このプロセスは_String Interningと呼ばれます。
  • JVMは、インターンされた文字列を、* _ * Stringテーブルと呼ばれる特別なネイティブの固定サイズのハッシュテーブルに保存します。* _ https://www.baeldung.com/java-string-pool [_String Pool _] としても知られています テーブルサイズを構成できます(つまり、 バケットの数)– XX:StringTableSize tuningフラグを介して。

    文字列テーブルに加えて、__ Runtime定数プールと呼ばれる別のネイティブデータ領域があります。 __JVMはこのプールを使用して、コンパイル時の数値リテラルや、実行時に解決する必要があるメソッドおよびフィールド参照などの定数を格納します。

2.6. ネイティブバイトバッファー

JVMは、かなりの数のネイティブ割り当ての通常の容疑者ですが、開発者がネイティブメモリを直接割り当てることもできます。 最も一般的なアプローチは、JNIおよびNIOのdirect _ByteBuffers._による__malloc __callです。

2.7. 追加の調整フラグ

このセクションでは、さまざまな最適化シナリオのために、いくつかのJVMチューニングフラグを使用しました。 次のヒントを使用すると、特定の概念に関連するほぼすべてのチューニングフラグを見つけることができます。
$ java -XX:+PrintFlagsFinal -version | grep <concept>
_PrintFlagsFinal_は、JVMのすべての-__ XX ___optionsを出力します。 たとえば、すべてのメタスペース関連フラグを検索するには:
$ java -XX:+PrintFlagsFinal -version | grep Metaspace
      // truncated
      uintx MaxMetaspaceSize                          = 18446744073709547520                    {product}
      uintx MetaspaceSize                             = 21807104                                {pd product}
      // truncated

3. ネイティブメモリトラッキング(NMT)

JVMのネイティブメモリ割り当ての一般的なソースがわかったので、次はそれらを監視する方法を見つけます。 **最初に、さらに別のJVMチューニングフラグ_-XX:NativeMemoryTracking = off | sumary | detailを使用して、ネイティブメモリトラッキングを有効にする必要があります。 _ **デフォルトでは、NMTはオフになっていますが、NMTを有効にして、観測の概要ビューまたは詳細ビューを表示できます。
典型的なSpring Bootアプリケーションのネイティブ割り当てを追跡したいとしましょう:
$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseG1GC -jar app.jar
ここでは、GCアルゴリズムとしてG1を使用して、300 MBのヒープスペースを割り当てながらNMTを有効にします。

3.1. インスタントスナップショット

NMTが有効になっている場合、__jcmd __commandを使用して、いつでもネイティブメモリ情報を取得できます。
$ jcmd <pid> VM.native_memory
JVMアプリケーションのPIDを見つけるには、__jps __コマンドを使用できます。
$ jps -l
7858 app.jar // This is our app
7899 sun.tools.jps.Jps
適切な_pid_とともにwith__jcmd __を使用すると、__VM.native_memory __はJVMにネイティブ割り当てに関する情報を出力させます。
$ jcmd 7858 VM.native_memory
NMT出力をセクションごとに分析してみましょう。

3.2. 総配分

NMTは、予約済みメモリとコミット済みメモリの合計を次のように報告します。
Native Memory Tracking:
Total: reserved=1731124KB, committed=448152KB
*予約済みメモリは、アプリが潜在的に使用できるメモリの総量を表します。 逆に、コミットされたメモリは、アプリが現在使用しているメモリ量に等しくなります。*
300 MBのヒープを割り当てていますが、アプリの合計予約メモリは約1.7 GBであり、それ以上です。 同様に、コミットされたメモリは約440 MBで、これも300 MBをはるかに超えています。
合計セクションの後、NMTは割り当てソースごとのメモリ割り当てを報告します。 それでは、各ソースを詳しく調べてみましょう。

3.3. Heap

NMTは、予想どおりにヒープ割り当てを報告します。
Java Heap (reserved=307200KB, committed=307200KB)
          (mmap: reserved=307200KB, committed=307200KB)
ヒープサイズの設定に一致する300 MBの予約済みメモリとコミット済みメモリ。

3.4. メタスペース

NMTがロードされたクラスのクラスメタデータについて言うことは次のとおりです。
Class (reserved=1091407KB, committed=45815KB)
      (classes #6566)
      (malloc=10063KB #8519)
      (mmap: reserved=1081344KB, committed=35752KB)
ほぼ1 GBが予約され、45 MBが6566クラスのロードにコミットされています。

3.5. 糸

スレッドの割り当てに関するNMTレポートは次のとおりです。
Thread (reserved=37018KB, committed=37018KB)
       (thread #37)
       (stack: reserved=36864KB, committed=36864KB)
       (malloc=112KB #190)
       (arena=42KB #72)
合計で、37 MBのスタックに36 MBのメモリが割り当てられます。スタックあたり約1 MBです。 JVMは作成時にメモリをスレッドに割り当てるため、予約済みとコミット済みの割り当ては等しくなります。

3.6. コードキャッシュ

JITによって生成およびキャッシュされたアセンブリ命令についてNMTが言っていることを見てみましょう。
Code (reserved=251549KB, committed=14169KB)
     (malloc=1949KB #3424)
     (mmap: reserved=249600KB, committed=12220KB)
現在、約13 MBのコードがキャッシュされており、この量は約245 MBに達する可能性があります。

3.7. GC

G1 GCのメモリ使用量に関するNMTレポートは次のとおりです。
GC (reserved=61771KB, committed=61771KB)
   (malloc=17603KB #4501)
   (mmap: reserved=44168KB, committed=44168KB)
ご覧のとおり、約60 MBが予約されており、G1の支援に専念しています。
メモリ使用量がどのように見えるかを見てみましょう。はるかに単純なGC、たとえばSerial GCの場合:
$ java -XX:NativeMemoryTracking=summary -Xms300m -Xmx300m -XX:+UseSerialGC -jar app.jar
シリアルGCは1 MBをほとんど使用しません。
GC (reserved=1034KB, committed=1034KB)
   (malloc=26KB #158)
   (mmap: reserved=1008KB, committed=1008KB)
明らかに、メモリ使用量のためだけにGCアルゴリズムを選択するべきではありません。SerialGCの世界を停止する性質により、パフォーマンスが低下する可能性があるからです。 ただし、https://www.baeldung.com/jvm-garbage-collectors [から選択する複数のGC]があり、それぞれがメモリとパフォーマンスのバランスが異なります。

3.8. シンボル

文字列テーブルや定数プールなどのシンボル割り当てに関するNMTレポートは次のとおりです。
Symbol (reserved=10148KB, committed=10148KB)
       (malloc=7295KB #66194)
       (arena=2853KB #1)
シンボルにはほぼ10 MBが割り当てられます。

3.9. 一定期間にわたるNMT

  • NMTを使用すると、メモリ割り当ての経時変化を追跡できます。*まず、アプリケーションの現在の状態をベースラインとしてマークする必要があります。

$ jcmd <pid> VM.native_memory baseline
Baseline succeeded
その後、しばらくして、現在のメモリ使用量とそのベースラインを比較できます。
$ jcmd <pid> VM.native_memory summary.diff
NMTは、記号と–記号を使用して、その期間にメモリ使用量がどのように変化したかを示します。
Total: reserved=1771487KB +3373KB, committed=491491KB +6873KB
-             Java Heap (reserved=307200KB, committed=307200KB)
                        (mmap: reserved=307200KB, committed=307200KB)

-             Class (reserved=1084300KB +2103KB, committed=39356KB +2871KB)
// Truncated
予約メモリとコミットメモリの合計は、それぞれ3 MBと6 MB増加しました。 メモリ割り当ての他の変動も簡単に発見できます。

3.10。 詳細なNMT

NMTは、メモリ空間全体のマップに関する非常に詳細な情報を提供できます。 この詳細レポートを有効にするには、__- XX:NativeMemoryTracking = detail __tuningフラグを使用する必要があります。

4. 結論

この記事では、JVMのネイティブメモリ割り当てへのさまざまな貢献者を列挙しました。 次に、実行中のアプリケーションを検査してネイティブの割り当てを監視する方法を学びました。 これらの洞察により、アプリケーションをより効果的に調整し、ランタイム環境のサイズを決定できます。