Java

Native Memory Tracking(NMT)とは?jcmdでJVMのネイティブメモリを調べる手順

Javaプロセスのメモリ使用量がヒープ設定値をはるかに超えて膨らみ、コンテナのメモリ上限でkillされる。この種の障害でヒープダンプを取っても答えは出ません。原因がヒープの外側、つまりJVMがOSから直接確保したネイティブメモリにあるからです。Native Memory Tracking(NMT)は、そのネイティブメモリをスレッドスタック・クラスメタデータ・コードキャッシュといったカテゴリに分けて報告するHotSpot VMの標準機能で、Java 7 Update 40(HotSpot 24)で導入されました。追加のエージェントもツールも要りません。ただしNMTが見せてくれる範囲には明確な境界があり、そこを知らずに使うと「NMTの合計値とOSが見ているメモリが合わない」という次の迷路に入ります。この記事では有効化から出力の読み方、追跡対象外の切り分け、JDKバージョンごとの仕様変更までを、OpenJDKのソースと公式ドキュメントを根拠に整理します。

まとめ:NMTで分かること、使う順番

  • NMTはJVM自身が確保したネイティブメモリをカテゴリ別に集計する機能。Javaヒープの内訳ではなく、ヒープ外の内訳を出す。
  • 有効化は起動時オプション -XX:NativeMemoryTracking=summary のみ。実行中のプロセスに後から有効化することはできない
  • レポート取得は jcmd <pid> VM.native_memory summary。増加分だけを見たいときは baseline を取ってから summary.diff
  • コストは公式ドキュメントで性能5〜10%、加えてmalloc 1件あたり18バイト(16バイトのヘッダ+2バイトのフッタ)。
  • JDKコアライブラリやサードパーティのJNIコードが確保したメモリはNMTに出ない。合計値がOSのRSSと一致しない主因はここ。
  • jcmd VM.native_memory shutdown はJDK 18で削除済み。Oracleのドキュメントには構文が残っているが実行できない。

以降は、この順番どおりに有効化・取得・読解・切り分けを追っていきます。

NMTが可視化するのはJVM内部のネイティブメモリ

ヒープ監視では見えない領域の内訳

Javaアプリケーションが使うメモリは、Javaヒープと、それ以外のネイティブメモリに大きく分かれます。Javaヒープとは?メモリ構造・サイズ設定・OutOfMemoryErrorの対処で扱うヒープ領域は -Xmx で上限を切れますが、スレッドスタック・クラスメタデータ・JITが生成したコード・GCの内部データ構造はヒープの外にあり、-Xmx では抑えられません。ヒープ使用率が安定しているのにプロセス全体のメモリが右肩上がり、という状況ではこちら側を疑うことになります。

NMTはこの「ヒープの外」をJVMの内側から集計します。OS側のツール(psやcgroupの統計)はプロセス全体の数字しか返しませんが、NMTはどのサブシステムが何バイト確保したかまで分解して返す点が決定的に違います。

NMTが分類するカテゴリ

集計単位はOpenJDKのソース(src/hotspot/share/nmt/memTag.hpp)で定義されたメモリタグです。レポートの見出しに出る主なものは次のとおりです。

表示名 内容
Java Heap Javaヒープ(合計のみ。世代別の内訳は出ない)
Class クラスメタデータ(Metaspaceと併記される)
Thread スレッドオブジェクト(スタックは stack: 行で内訳表示)
Code JITが生成したコード(コードキャッシュ)
GC / GCCardSet GCの内部構造、G1のカードセット
Compiler コンパイラの作業領域
Symbol シンボルテーブル
Internal 上記に属さないVM内部の確保
Other JVM自身の用途ではない確保(Unsafe経由など)
Native Memory Tracking NMT自身が使うメモリ

ソースのコメントは Internal を「VMが使うが上記のどのカテゴリにも属さないもの」、Other を「VMが使うのではないメモリ」と区別しています。この2つは名前が似ていて混同されがちですが、調査の意味はまったく違います。Internal が伸びていればJVM内部の何か、Other が伸びていればアプリケーションコードが確保したダイレクトバッファ類が候補になります。なお mtThreadStack というタグは内部的に存在しますが、レポート上は Thread に統合して表示されます。

NMTを有効にする:-XX:NativeMemoryTracking の指定

NMTは起動時オプションで有効にします。値は offsummarydetail の3つで、製品ビルドの既定値は off です(デバッグビルドの既定は summary)。

java -XX:NativeMemoryTracking=summary -jar app.jar
java -XX:NativeMemoryTracking=detail  -jar app.jar

summaryとdetailの選び分け

summary はカテゴリ単位の集計だけを保持します。detail はこれに加えて確保が起きた呼び出し箇所(コールスタック)を記録し、jcmddetail レポートで「どのコードパスが確保したか」まで出せます。対象は1件1KB以上の確保があったコールサイトで、これはコマンドのヘルプ文(”report memory allocation >= 1K by each callsite”)にも明記されています。ネイティブメモリリークの犯人がJVM内部のどこかにある場合、最終的にはこのモードが必要になります。

選択の基準ははっきりしています。常時つけておくのは summary だけdetail はコールスタックの収集と保持が乗るため、原因のあたりが付いてから、再現環境か短時間の本番検証に限って使います。「とりあえずdetailで起動しておく」は、NMT自身のメモリ消費(レポートに Native Memory Tracking カテゴリとして出ます)と性能低下の両方を無駄に払うことになります。

JVM終了時にレポートを残す指定

障害の瞬間に jcmd を打てるとは限りません。診断オプション -XX:+PrintNMTStatistics を付けておくと、JVM終了時にNMTのサマリが自動的に出力されます。短命なバッチプロセスや、落ちた後でしか気づけないケースで効きます。

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics -XX:NativeMemoryTracking=summary -jar app.jar

起動後の有効化・停止の可否

NMTは実行中のJVMに対して有効化できませんoff で起動したプロセスの内訳を後から調べる方法は、オプションを付けて再起動する以外にありません。障害が起きてから調べようとして詰むのはこのためで、メモリ挙動が読めないうちは summary を付けて起動しておくのが実務的です。

逆方向、つまり動作中のNMTを止める jcmd <pid> VM.native_memory shutdown も、JDK 18(2022年3月リリース)で削除されました(JDK-8277947)。注意が必要なのは、Oracleの公式ドキュメントに載っている構文行にはJDK 25版でも shutdown が残っている点です。ドキュメントを見て実行しても未知の引数として弾かれるので、現在のOpenJDK実装が受け付けるオプションを次節の表で確認してください。

jcmd VM.native_memory の使い方と出力の読み方

サブコマンドとscale指定

レポートは jcmd で取得します。プロセスIDは jcmd -l で確認できます。

jcmd <pid> VM.native_memory summary scale=MB

OpenJDKの現行実装(src/hotspot/share/nmt/nmtDCmd.cpp)が受け付けるオプションは次のとおりです。

オプション 動作 detail起動が必要
summary カテゴリ別の集計を表示(省略時の既定) 不要
detail 1KB以上を確保したコールサイト別の内訳も表示 必要
baseline 現時点のスナップショットを基準として記録 不要
summary.diff 基準との差分をカテゴリ別に表示 不要
detail.diff 基準との差分をコールサイト別に表示 必要
statistics NMT自身の内部統計を表示 JDK 23以降は不要
scale=1|B|KB|MB|GB 出力単位の指定(既定はKB) 不要

複数のオプションを同時に指定するとエラーになります。statistics はJDK 22以前では detail 起動が前提でしたが、JDK 23で summary モードでも動作するようになりました(JDK-8331858)。scale=1(バイト単位)を指定すると小さなカテゴリが省略されなくなるため、内訳を漏れなく見たいときに使えます。

reserved・committed・peakの意味

レポートは全体合計に続いて、カテゴリごとに次の形式で並びます(数値は環境依存のためプレースホルダで示します)。

Native Memory Tracking:

(Omitting categories weighting less than 1MB)

Total: reserved=NNN, committed=NNN
       malloc: NNN #NNN, peak=NNN #NNN
       mmap:   reserved=NNN, committed=NNN

-                 Java Heap (reserved=NNN, committed=NNN)
                            (mmap: reserved=NNN, committed=NNN, peak=NNN)

reserved は仮想アドレス空間として予約した量、committed はそのうちOSにコミットを要求した量です。障害調査で見るべきなのは committed のほうで、reserved はヒープの最大サイズなど「上限として押さえてあるだけ」の数字を含むため、実消費とは桁が違うことがよくあります。

peak は期間中の最大値で、リリースビルドでも表示されるようになったのはJDK 22からです(JDK-8317772)。取得した瞬間だけ小さく見えるスパイク型の増加を、1回のレポートで拾えます。ただし peak が出るのは malloc の合計行とカテゴリごとの mmap 行で、先頭の Total: 行には出ません。JDK 21以前ではこの列自体が無いため、増加を追うには次節のベースライン差分が必須になります。

なお Native Memory Tracking カテゴリの行には tracking overhead= が併記され、NMT自身が消費している量が分かります。「NMTを入れたせいでどれだけ増えたのか」を推測ではなく数字で確認できます。また scale にKB以上を指定した場合は必ず (Omitting categories weighting less than 1KB) のような注記が出力され、1単位に満たないカテゴリは一覧から省かれます。カテゴリが見当たらないときは、消えたのではなく閾値未満だと考えてください。

ベースライン差分による増加分の抽出

ネイティブメモリの調査では、絶対値よりも「どの区間で、どのカテゴリが、どれだけ増えたか」が答えに直結します。NMTのベースライン機能はそのための仕組みです。

jcmd <pid> VM.native_memory baseline
jcmd <pid> VM.native_memory summary.diff scale=MB

手順としては、アプリケーションが定常状態に入った時点で baseline を打ち、疑わしい処理(バッチ実行、負荷試験、該当APIの連続呼び出しなど)を通してから summary.diff を取ります。差分レポートは各カテゴリに + / - の増減が付くため、Thread が増え続けていればスレッドの解放漏れ、Class が増え続けていればクラスローダのリークといった具合に、調査対象を一気に絞れます。

時間をおいて複数回 summary.diff を取れば、増加が線形か頭打ちかも判別できます。頭打ちならウォームアップに伴う正常な増加、線形に伸び続けるならリークの可能性が高い、という切り分けです。リーク一般の追い方はメモリリークとは?原因の特定方法と直し方を言語・環境別に解説で整理しています。

オーバーヘッドは性能5〜10%+malloc 1件あたり18バイト

NMTのコストの内訳

Oracleの公式ドキュメントは「Enabling NMT causes a 5% -10% performance overhead.」と明記しています。これは公式が示す目安であり、実際の値はワークロード依存です。加えてメモリ側のコストがあり、こちらは公式ドキュメントより実装を見たほうが正確です。OpenJDKの mallocHeader.hppSTATIC_ASSERT(sizeof(MallocHeader) == (sizeof(uint64_t) * 2)) でヘッダを16バイトに固定しており(64bit環境でも32bit環境でも16バイト)、これに2バイトのフッタが付きます。つまりNMT有効時はJVMがmallocする1ブロックごとに18バイトが上乗せされます。Java 8時代のドキュメントにある「2ワードのヘッダ」という記述より、現行実装ではフッタのぶんだけ多いことになります。小さな確保が大量に発生するワークロードほど、この定数が効いてきます。

一方、NMTを off にしているときにも残っていたコストは、JDK 20で解消されました(JDK-8296437「NMT incurs costs if disabled」)。無効時の負荷を根拠にNMTの導入を見送っているなら、その前提はJDK 20以降では古くなっています。

本番で summary を常時有効にする判断基準

結論から言えば、ネイティブメモリの挙動が把握できていないサービスでは summary を常時有効にすべきです。5〜10%を払えないほど余裕のないサービスであれば、そもそも「原因不明のメモリ増加で落ちる」リスクのほうが高くつきます。NMTが無効なプロセスは後から調べる手段がなく、再起動=証拠の消滅だからです。

採用すべきでない場面もはっきりしています。レイテンシのパーセンタイルが厳密なSLOに縛られていて数%の劣化も許容できないシステム、あるいは短命なプロセスを大量に起動するバッチ基盤では、常時有効化ではなく再現環境での取得に留めるべきです。detail の常時有効化はどのケースでも推奨しません。

NMTで犯人が出ないときの切り分け

JDKコアライブラリとJNIコードは追跡対象外

NMTの集計対象はHotSpot VM自身の確保です。ファイルI/O、ネットワーク、圧縮、暗号といったJDKコアライブラリのネイティブ実装や、アプリケーションが読み込んだサードパーティのJNIライブラリが直接 malloc した領域は、NMTのどのカテゴリにも計上されません。NMTの合計committedとOSが報告するRSSが数百MB単位で食い違うとき、まずここを疑うことになります。

この構造的な穴はOpenJDK側でも課題として扱われており、JDKコアライブラリのネイティブ確保をNMTのカテゴリに割り当てる土台を作るJEP(JDK-8354416「Prepare for Native Memory Tracking in the JDK」)が提案されています。2026年7月時点のステータスはSubmittedで、個別ライブラリの対応自体はNon-Goalsに置かれています。近い将来のJDKで自動的に解決する話ではないため、現時点ではNMTの外側を別手段(jemallocのプロファイル、pmap によるマッピング確認など)で押さえる前提で調査計画を立ててください。

DirectByteBufferは「Other」に出る

ダイレクトバッファやUnsafe経由の確保がどこに出るかは頻出の疑問です。OpenJDKの unsafe.cpp では Unsafe_AllocateMemory0os::malloc(sz, mtOther) を呼んでおり、Other カテゴリに計上されますjava.nio.ByteBuffer.allocateDirect() で確保したメモリもこの経路です。

したがって Other が単調増加している場合、疑うべきはJVMではなくアプリケーションコードです。ダイレクトバッファの解放がGC任せになっている(参照が残っていて回収されない)、NIOのバッファプールが肥大している、といった典型パターンに当てはまることが多く、ここが分かるだけで調査範囲はJVMチューニングからアプリケーション実装へ切り替わります。Javaのガベージコレクション(GC)とは|仕組み・種類・チューニングの基礎で扱うGCの調整では解決しない領域です。

MallocLimitによる確保現場の捕捉

増え続けるカテゴリまで特定できたら、次は「そのメモリを確保している瞬間のスタック」が欲しくなります。NMTが有効なときだけ使える診断オプション -XX:MallocLimit が有効です。指定した量に達した時点でVMを止められるため、事後のレポートではなく確保の現場を捕まえられます。

java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:MallocLimit=compiler:200m:oom,code:100m -jar app.jar

書式は全体上限なら -XX:MallocLimit=2g、カテゴリ別なら上の例のように <category>:<size>[:<flag>] をカンマ区切りで並べます。到達時の動作は fatal(確保箇所でVMを致命的エラー終了、既定)と oom(ネイティブOOMを模倣)の2種類です。JDK 21でこの書式に刷新されました(JDK-8293313)。診断オプションなので -XX:+UnlockDiagnosticVMOptions が必要で、当然ながら本番で常用するものではなく、再現環境で犯人を確定させるための道具です。

JDKバージョンで変わったNMTの仕様

NMTは長期にわたって改修が続いている領域で、手元のJDKによって使えるオプションも出力も変わります。実務に影響する変更点は次のとおりです。

JDK 変更内容 課題番号
18 VM.native_memory shutdown を削除 JDK-8277947
20 無効時に残っていたコストを解消 JDK-8296437
21 MallocLimit の書式を刷新(カテゴリ別・fatal/oom) JDK-8293313
22 peak値をリリースビルドでも表示 JDK-8317772
22 カテゴリごとの largest_committed を追加 JDK-8293850
22 detailのコールスタックにライブラリ名を表示 JDK-8319437
23 statistics が summary モードでも動作 JDK-8331858
24 コールスタックにソース情報(ファイル・行)を表示 JDK-8333994

調査記事やStack Overflowの回答を参照する際は、その内容がどのJDKを前提にしているかを必ず確認してください。特にJDK 21以前を運用している環境では、peak表示がないぶんベースライン差分の重要度が上がります。実行中のJDKバージョンは java -version で確認できます。JVMそのものの構造はJava仮想マシン(JVM)とは何かをわかりやすく解説で整理しています。

よくある質問

NMTは本番環境で常時有効にしてよいですか

summary であれば有効にしておく判断を推奨します。公式ドキュメントの性能オーバーヘッドは5〜10%で、これはメモリ障害の原因を後から一切追えなくなるリスクと比較して払う価値があります。ただしレイテンシSLOが厳しいシステムでは、常時有効化ではなく再現環境での取得に切り替えてください。detail の常時有効化は推奨しません。

-XX:NativeMemoryTracking=detail は起動後に切り替えられますか

できません。NMTのモードは起動時オプションで固定され、JDK 18以降は実行中のJVMに対する有効化・モード変更・停止のいずれも不可能です(JDK 17以前は shutdown による停止のみ可能でした)。detail レポートが必要になった時点で、そのオプションを付けて再起動する必要があります。

NMTのcommitted合計とOSが表示するメモリ使用量が合わないのはなぜですか

主因は2つです。1つはNMTがHotSpot VM自身の確保しか追跡しないこと。JDKコアライブラリやサードパーティのJNIコードが確保した領域はレポートに現れません。もう1つはcommittedがOSへのコミット要求量であり、実際に物理メモリに載っている量(RSS)とは定義が違うことです。コミット済みでも一度も触られていないページは物理メモリを消費しません。

レポートで最初に見るべきカテゴリはどこですか

プロセス全体の増加を追っている場合は、まず Total の committed とOSのRSSを突き合わせ、乖離が小さければ内訳へ進みます。内訳では Thread(スレッドの解放漏れ)、Class と Metaspace(クラスローダのリーク)、Code(コードキャッシュの肥大)、Other(アプリケーションのダイレクトバッファ)の4つが実務での頻出箇所です。乖離が大きい場合はNMTの追跡対象外を疑う段階に移ります。

DirectByteBufferの使用量はNMTのどこに出ますか

Other カテゴリに出ます。ただしここに計上されるのは Unsafe.allocateMemory 経由の確保のみで、JNIライブラリが自前で malloc した領域は含まれません。Other が増えていないのにプロセスのメモリが増える場合は、NMTの外側を調べる必要があります。

関連記事

資料請求

RELATED POSTS 関連記事