Javaプロセスのメモリがヒープの上限をはるかに超えて膨らみ、コンテナのメモリ上限で落とされます。この種の障害は、ヒープダンプを取っても原因が出てきません。増えているのがヒープの外、つまりJVMがOSから直接確保したネイティブメモリだからです。Native Memory Tracking(NMT)は、そのネイティブメモリをスレッドスタック・クラスメタデータ・コードキャッシュといったカテゴリに分けて報告するHotSpot VMの標準機能です。この記事では、JDK 25.0.4.1のLinuxコンテナで実際に取った出力をもとに、有効化から読み方、NMTに出ないメモリの探し方までを説明します。
まとめ:NMTで分かること・分からないこと
- 起動時に
-XX:NativeMemoryTracking=summaryを付け、jcmd <pid> VM.native_memory summaryで読む。実行中のプロセスに後から有効化する方法は無い。 - 増えたカテゴリを探すときは
baselineを打ってからsummary.diff。DirectByteBufferを1MiBずつ104個溜めた実験では、Otherの行に「+104MB」「#141 +104」と件数まで出た。 - DirectByteBufferとFFMの
Arenaで確保したメモリはOtherに出る。FFMやJNIのネイティブコードが直接mallocした256MBは、RSSには載るのにNMTのどこにも出なかった。 - NMTの外にあるメモリは
jcmd <pid> System.map(Linux JDK 22以降・21.0.8以降)で探す。VMが把握していない領域は info 欄が「-」で並ぶ。 - コストはOracle公式の目安で性能5〜10%、加えてJVMがmallocする1件ごとに18バイト。原因の分からないメモリ増加を抱えるサービスなら、summary の常時有効化は払う価値がある。
VM.native_memory shutdownはJDK 18と17.0.7で削除済み。Oracleの公式ドキュメントにはJDK 27版でも構文が残っている。
以下、有効化→出力の読み方→差分→計上先の切り分け→NMTの外側の調査、の順に進めます。
NMTが集計するのはJVM自身のネイティブメモリ
Javaプロセスのメモリは、Javaヒープと、それ以外のネイティブメモリに分かれます。ヒープは -Xmx で上限を切れますが、スレッドスタック・クラスメタデータ・JITが生成したコード・GCの管理領域はヒープの外にあり、-Xmx では抑えられません。ヒープ使用率は落ち着いているのにプロセス全体のメモリが増え続けるなら、疑うのはこちら側です。
OSのツール(ps のRSSやcgroupの統計)が返すのはプロセス全体の数字だけです。NMTはJVMの内側から、どのサブシステムが何バイト確保したかを分解して返します。JVM全体のメモリ構成はJava仮想マシン(JVM)の仕組みとメモリ構成で整理しています。
NMTのカテゴリ一覧とJDK 26・27での変化
集計の単位はOpenJDKのソース memTag.hpp で定義されたメモリタグです。調査で見ることの多いものを挙げます。
| 表示名 | 中身 | 見るべき場面 |
|---|---|---|
| Java Heap | Javaヒープ | -Xmxとの突き合わせ |
| Class / Metaspace | クラスメタデータ | クラスローダのリーク |
| Thread | スレッドとそのスタック | スレッドの作りすぎ |
| Code | JITのコードキャッシュ | コードキャッシュ肥大 |
| GC / GCCardSet | GCの管理領域 | GC方式の変更後 |
| Internal | VM内部の分類外の確保 | JVM側の問題 |
| Other | VMが使うのではない確保 | アプリのダイレクトメモリ |
| JNI | jmethodID等(JDK 26〜) | JNI・JVMTIの多用時 |
| Native Memory Tracking | NMT自身 | NMTのコスト確認 |
InternalとOtherは名前が似ていますが、調査の行き先が違います。ソースのコメントはInternalを「VMが使うが他のどこにも属さないもの」、Otherを「VMが使うのではないメモリ」と書き分けています。Internalが伸びていればJVMの内部、Otherが伸びていればアプリケーションのコードが候補です。ただしJDK 27までは、JNIの呼び出しに伴ってJVMが確保する領域の一部もInternalに入るため、JNIを多用するアプリケーションではInternalの増加もアプリケーション側の使い方が原因になりえます。
カテゴリは版によって増減します。JDK 26で「JNI」が加わりました(JDK-8268406でjmethodIDの領域に付けたタグ)。ここに出るのはJVMがJNIのために確保した領域で、アプリケーションのJNIライブラリが自分で確保したメモリではありません。JDK 27ではJVMCIの除去(JDK-8382582)に伴い「JVMCI」タグが消えました。memTag.hppの jdk-25+36 と jdk-27+35 を比べると、差分はこの2つだけです。開発中のJDK 28では、Internalに混ざっていたJNI側の確保をJNIへ移す変更(JDK-8392000)も入っています。
-XX:NativeMemoryTracking=summary による起動とモードの選び分け
NMTは起動オプションで有効にします。値は off・summary・detail の3つで、既定は off です。
java -XX:NativeMemoryTracking=summary -jar app.jar
java -XX:NativeMemoryTracking=detail -jar app.jar
Kubernetesなどでコマンドラインを触りにくい場合は、環境変数 JAVA_TOOL_OPTIONS=-XX:NativeMemoryTracking=summary でも渡せます。
summaryとdetailの選び分け
summaryはカテゴリ単位の集計だけを持ちます。detailはそれに加えて確保が起きた呼び出し元(コールスタック)を記録し、1KB以上を確保したコールサイトごとの内訳を出せます。JDK 25でdetailのレポートを取ると、次のように確保元の関数名が並びます。
[0x00007f873ddd8000 - 0x00007f873ddd9000] reserved and committed 4KB for Internal from
[0x00007f873cb21a5e]initialize_assert_poison()+0x1e
[0x00007f873d59d4ad]Threads::create_vm(JavaVMInitArgs*, bool*)+0x53d
[0x00007f873ce75aaf]JNI_CreateJavaVM+0x4f
[0x00007f873dde39df]JavaMain+0x8f in libjli.so
NMT自身の消費量も出力の「Native Memory Tracking」行で分かります。同じ何もしないプログラムで比べると、summaryは105KB、detailは289KB(うちmallocの件数が75件から1,845件へ増加)でした。起動直後でこの差なので、確保の多いアプリケーションではさらに開きます。常時付けておくのはsummaryだけにし、detailは原因の当たりが付いてから再現環境で使う、という分け方で十分です。
JVM終了時にレポートを残す PrintNMTStatistics
短命なバッチや、落ちてから気づく障害ではjcmdを打つ暇がありません。-XX:+PrintNMTStatistics を付けると、JVMの終了時にsummaryが出力されます。これは診断用フラグなので、前に -XX:+UnlockDiagnosticVMOptions が必要です。付け忘れると「VM option ‘PrintNMTStatistics’ is diagnostic and must be enabled via -XX:+UnlockDiagnosticVMOptions.」と出てJVMが起動しません。
java -XX:NativeMemoryTracking=summary -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics -jar app.jar
OOM Killerのように外からSIGKILLで殺された場合は、終了処理が走らないので出力は残りません。コンテナでの強制終了に備えるなら、後述のJFRで時系列を記録しておくほうが確実です。
起動後の有効化・停止ができない制約
NMTを付けずに起動したプロセスにjcmdを打っても、返ってくるのは「Native memory tracking is not enabled」の1行だけです。モードの変更も含め、オプションを付けて再起動する以外に手はありません。障害が起きてから調べようとして行き詰まるのは、たいていここです。
逆方向の jcmd <pid> VM.native_memory shutdown(NMTの停止)も、JDK 18で削除されました(JDK-8277946)。この削除はJDK 17.0.7にもバックポートされています(JDK-8299846)。ところがOracleのJDK 25版ガイドは、構文行と表に今も shutdown を載せています。JDK 27版も同じです。ドキュメントどおりに打っても受け付けられないので、使えるオプションは次の表かjcmdのヘルプで確かめてください。
jcmd VM.native_memory summary の出力の読み方
jcmdのオプション一覧
プロセスIDは jcmd -l で確認できます。JDK 25の jcmd <pid> help VM.native_memory が示すオプションは次のとおりです。
| オプション | 動作 | detail起動が必要 |
|---|---|---|
| summary | カテゴリ別の集計(省略時も同じ) | 不要 |
| detail | 1KB以上のコールサイト別の内訳 | 必要 |
| baseline | 現時点を基準として記録 | 不要 |
| summary.diff | 基準とのカテゴリ別の差分 | 不要 |
| detail.diff | 基準とのコールサイト別の差分 | 必要 |
| statistics | NMT自身の内部統計 | JDK 23以降は不要 |
| scale= | 単位(KB・MB・GB。既定はKB) | 不要 |
summaryとdetailのように2つを同時に指定すると、「At most one of the following option can be specified」で弾かれます。statisticsはJDK 22まではdetail起動が前提でしたが、JDK 23でsummaryでも動くようになりました(JDK-8331858)。ヘルプに載っているのはKB・MB・GBだけですが、scale=1 や scale=B でもバイト単位で出力されました。
JDK 25の実際の出力
メモリ上限2GBのコンテナで、何もしないプログラムを -XX:NativeMemoryTracking=summary 付きで起動し、scale=MB で取った出力の前半です(Classの内訳8行は省略)。
Native Memory Tracking:
(Omitting categories weighting less than 1MB)
Total: reserved=1930MB, committed=101MB
malloc: 12MB #5696, peak=11MB #5698
mmap: reserved=1918MB, committed=89MB
- Java Heap (reserved=512MB, committed=34MB)
(mmap: reserved=512MB, committed=34MB, at peak)
- Class (reserved=1024MB, committed=0MB)
(classes #877)
( instance classes #735, array classes #142)
(mmap: reserved=1024MB, committed=0MB, at peak)
- Thread (reserved=18MB, committed=1MB)
(threads #18)
(stack: reserved=18MB, committed=1MB, peak=1MB)
- Code (reserved=242MB, committed=7MB)
(mmap: reserved=242MB, committed=7MB, at peak)
- GC (reserved=50MB, committed=41MB)
(malloc=8MB tag=GC #345) (at peak)
(mmap: reserved=42MB, committed=33MB, at peak)
先頭の「(Omitting categories weighting less than 1MB)」は、指定した単位に満たないカテゴリを省いたという注記です。scale=MB ではOtherやTracingのような小さなカテゴリが一覧から消えるので、見当たらないカテゴリは「無い」のではなく「1MB未満」と読みます。# の後ろの数字は確保の件数で、Javaヒープの512MBはコンテナ上限2GBの4分の1です(既定の最大ヒープ。詳しくはJavaヒープの既定サイズの実測)。
reserved・committed・peakとRSSの関係
reservedは仮想アドレス空間として予約した量、committedはそのうちOSにコミットを求めた量です。上の出力でClassのreservedが1,024MBあるのは、圧縮クラスポインタ用のクラス空間を1GB予約しているからで、物理メモリは使っていません。調査で見るのはcommittedです。
ただしcommittedもRSS(実際に物理メモリに載っている量)とは一致しません。同じプロセスのRSSは /proc/<pid>/status で41MBで、NMTのcommitted合計101MBの半分以下でした。コミットされていても、一度も書き込まれていないページは物理メモリを消費しないからです。NMTのcommittedは「最大でここまで載りうる量」で、RSSと一致しないこと自体は異常ではありません。RSSとスワップの関係はスワッピングとページングの違いで扱っています。
peakは期間中の最大値です。リリースビルドで表示されるようになったのはJDK 22からで(JDK-8317772)、21.0.2と17.0.10にもバックポートされました。これより前の版の出力にはこの列がありません。一瞬だけ膨らんで戻るタイプの増加を1回のレポートで拾えるので、peakが出る版ではまずpeakとcommittedの差を見ます。
baselineとsummary.diffによる増加カテゴリの特定
ネイティブメモリの調査で効くのは絶対値より差分です。定常状態に入った時点で baseline を打ち、疑わしい処理を通してから summary.diff を取ります。
jcmd <pid> VM.native_memory baseline
jcmd <pid> VM.native_memory summary.diff scale=MB
50ミリ秒ごとに1MiBのDirectByteBufferを確保して保持し続けるプログラムで、baselineから約5秒後に取った差分のうち、TotalとOtherの行です。
Total: reserved=2070MB +104MB, committed=241MB +104MB
malloc=152MB +104MB #4911 +153
mmap: reserved=1918MB, committed=89MB
- Other (reserved=141MB +104MB, committed=141MB +104MB)
(malloc=141MB +104MB #141 +104)
増えたのがOtherだけで、件数も104件増えていることが読み取れます。1件あたり1MBなので「1MBの確保が104回、解放されずに残っている」と分かり、DirectByteBufferの保持を疑う段階まで絞れます。時間をおいて何度か summary.diff を取り、増加が頭打ちならウォームアップ、線形に伸び続けるならリークと判断します。リーク一般の追い方はメモリリークの原因の特定方法にまとめています。
確保方法ごとの計上先:DirectByteBuffer・FFM・スレッドの実測
「どの確保がNMTのどこに出るか」を、確保方法を1種類ずつ変えて測りました。いずれもJDK 25.0.4.1のLinuxコンテナで、確保後にページへ書き込んで物理メモリに載せてから、summaryとRSSを取っています。
| 確保方法 | NMTの計上先 | NMTの増分 | RSS |
|---|---|---|---|
| 確保なし(基準) | - | - | 41MB |
| allocateDirect 64MB×4 | Other | 256MB(#6) | 297MB |
| FFM Arena 256MB | Other | 256MB(#3) | 306MB |
| FFMでlibcのmalloc 256MB | どこにも出ない | 8MB(Arena Chunk等) | 311MB |
| プラットフォームスレッド500本 | Thread | reserved +503MB | 89MB |
| 仮想スレッド1万本 | Threadに出ない | threads #25止まり | 80MB |
RSSは /proc/<pid>/status のVmRSSを1MB未満切り捨てで示した値です。仮想スレッドの行だけは別の測定(スレッドを待機させるだけのプログラム)です。
DirectByteBufferとFFMのArenaの計上先「Other」
ByteBuffer.allocateDirect() も、FFM(Foreign Function & Memory API)の Arena.global().allocate() も、内部では Unsafe.allocateMemory を通ってJVMの os::malloc で確保されるため、Otherに計上されました。Otherが単調に増えているなら、疑うのはJVMではなくアプリケーションです。参照が残ってダイレクトバッファが回収されない、NIOのバッファプールが肥大している、FFMの Arena を閉じ忘れている、といったパターンが典型です。これはGCの調整では直らない領域で、JavaのGCのチューニングを見直しても数字は動きません。
FFM・JNIで直接mallocした領域の計上漏れ
FFMの Linker でlibcの malloc を直接呼んで256MBを確保すると、RSSは311MBまで増えたのに、NMTの合計は確保前とほぼ同じ(Otherの行も現れない)でした。NMTが数えるのはHotSpot VMが自分の os::malloc と mmap を通して確保した分だけです。JNIのネイティブライブラリ、FFMから呼んだCのコード、JDKのクラスライブラリのネイティブ実装(圧縮・暗号など)が直接 malloc した領域は、どのカテゴリにも入りません。
「NMTの合計とRSSが数百MB食い違う」とき、まず疑うのはここです。OpenJDKでも、JDKのクラスライブラリの確保をNMTに載せる土台を作るJEP(JDK-8354416)が出ていますが、2026年10月時点の状態はSubmittedのままです。当面はNMTの外側を次章の方法で調べる前提で計画を立てます。
スレッドスタックの計上先と仮想スレッドの扱い
プラットフォームスレッドを500本立てると、Threadのreservedは18MBから521MBへ、committedは1MBから51MBへ増えました。1本あたり約1MBのスタックを予約し、そのうち実際に使った分だけがコミットされるためです。スレッドプールの上限を設けずにスレッドが増え続けるアプリケーションでは、Threadの threads # の数がスレッド数そのものなので、増加はそこに出ます。
一方、仮想スレッドを1万本立てて待機させても、Threadの行は threads #25(基準の18本にキャリアスレッド等が加わった数)にとどまりました。仮想スレッドのスタックはヒープ上のオブジェクトとして保存されるので、NMTではJava Heapの側に含まれます。仮想スレッドへ移行したアプリケーションで「Threadが減ったのにメモリが減らない」ように見えるのはこのためです。
jcmd System.map によるNMTの外側のメモリ調査
NMTの合計とRSSの差が大きいときは、jcmd <pid> System.map でプロセスのメモリマップを、NMTが知っている用途の注釈付きで出せます。Linux版はJDK 22で入り(JDK-8318636)、21.0.8にもバックポートされました。WindowsはJDK 24、macOSはJDK 25から使えます。
先ほどのFFMで256MBをmallocしたプロセスで実行し、RSSが20MBを超える領域だけを抜き出すと、該当するのは1行だけでした。
from to vsize prot rss hugetlb pgsz notes info file
0x000079795bfff000-0x000079796c021000 268574720 rw-p 268443648 0 4K com - -
Javaヒープなら JAVAHEAP、スレッドスタックなら STACK-313-main のように、VMが把握している領域にはinfo欄に用途が入ります。256MBの領域はinfoもfileも「-」で、VMの外で確保された匿名メモリだと分かります。出力の最後には全体のRSS(このときは312M)も出るので、NMTのcommittedとの差を同じ画面で確かめられます。
info欄を埋めるにはNMTが必要です。NMTを付けずに起動したプロセスでは「NMT is disabled. VM info not available.」と出て、マップは出ても用途が空になります。ここで「VMの外」と分かったら、次はネイティブ側のツールの出番です。glibcのmallocならjemallocやtcmallocへ差し替えてプロファイルを取る、ネイティブライブラリの呼び出し箇所を洗う、といった調査に進みます。
JFRによるNMTの値の時系列記録
jcmdは打った瞬間の値しか返しません。JDK 20以降はNMTの値がJFR(JDK Flight Recorder)のイベントになっており(JDK-8157023)、jdk.NativeMemoryUsage(カテゴリ別)と jdk.NativeMemoryUsageTotal(合計)が記録されます。JDK 25同梱の default.jfc と profile.jfc は、どちらもこの2つを1秒周期で有効にしています。
java -XX:NativeMemoryTracking=summary -XX:StartFlightRecording=filename=rec.jfr -jar app.jar
jfr print --events jdk.NativeMemoryUsage rec.jfr
DirectByteBufferで256MBを確保したプロセスの記録からは、次のイベントが取れました。
jdk.NativeMemoryUsage {
startTime = 10:30:23.187 (2026-10-28)
type = "Other"
reserved = 256.0 MB
committed = 256.0 MB
}
注意したいのは、filename に指定したファイルは記録の停止時に書かれることです。OOM KillerのSIGKILLで殺されると rec.jfr は作られません。JFRは記録中のデータをリポジトリ(既定はシステムの一時ディレクトリ)にチャンクとして書き出しているので、残ったチャンクを jfr assemble <リポジトリ> rec.jfr で1本にまとめれば、直前までのカテゴリ別の推移を読めます。コンテナが作り直されると一時ディレクトリごと消えるため、-XX:FlightRecorderOptions:repository= で永続ボリューム上のディレクトリを指定しておきます。-XX:StartFlightRecording に maxage や maxsize を付けて保存期間を区切っておけば、本番で常時記録しても容量は膨らみません。PrintNMTStatisticsが届かないSIGKILLの障害は、JFRのリポジトリか、jcmdを定期実行して残した記録から追います。
MallocLimitによる確保時点での停止
増え続けるカテゴリが分かったら、確保している瞬間のスタックが欲しくなります。-XX:MallocLimit は、malloc量の合計またはカテゴリごとの量が上限に達した時点でJVMを止める診断用フラグです。書式はJDK 21で今の形になりました(JDK-8293313)。
java -XX:NativeMemoryTracking=summary -XX:+UnlockDiagnosticVMOptions -XX:MallocLimit=other:100m -jar app.jar
Otherに100MBの上限を付け、64MBのDirectByteBufferを4つ確保するプログラムを動かすと、2つ目の確保でJVMが致命的エラーで止まりました(終了コード134)。
# Internal Error (mallocTracker.cpp:149), pid=53, tid=54
# fatal error: MallocLimit: reached category "mtOther" limit (triggering allocation size: 65536K, allocated so far: 65536K, limit: 100M)
このときhs_errファイルと、ulimit -c が0でなければコアダンプも書き出され、上限に達した確保のスタックをそこから読めます。メモリ上限2GBのコンテナで2回実行した記録では、コアダンプは164MBと227MBでした。出力先の空き容量に注意してください。上限の後ろに :oom を付けた other:100m:oom では、JVMを致命的エラーで止めずにその確保だけを失敗させます。今回のプログラムでは、Java側に「java.lang.OutOfMemoryError: Unable to allocate 67108864 bytes」が Unsafe.allocateMemory のスタック付きで飛びました。
NMTを付けずに MallocLimit だけを指定すると、「MallocLimit will be ignored since NMT is disabled.」と警告が出て上限は効きません。本番に常用するものではなく、再現環境で犯人を確定させるための道具です。
オーバーヘッドと本番での常時有効化の判断
NMTのコストの内訳
Oracleの公式ドキュメントは「Enabling NMT causes a 5% -10% performance overhead.」と書いています。これは目安で、実際の値は確保の頻度に左右されます。メモリ側のコストは実装を見るほうが正確です。OpenJDKのmallocHeader.hppはヘッダを16バイトに固定しており、これに2バイトのフッタが付きます。NMTを有効にすると、JVMがmallocする1件ごとに18バイトが上乗せされる計算です。
NMTを off にしているときにも残っていたコストは、JDK 20で解消されました(JDK-8296437)。「無効でも負荷がある」という理由でNMTを避けているなら、その前提はJDK 20以降と17.0.10以降では成り立ちません。
summaryを常時有効にする判断基準
ネイティブメモリの挙動を把握できていないサービスでは、summaryを常時有効にしておくべきです。NMTが無効なプロセスは後から調べようがなく、再起動すれば証拠も消えます。原因不明のメモリ増加で落ち続けるリスクに比べれば、5〜10%は安い保険です。
常時有効にすべきでないのは、レイテンシのパーセンタイルが厳しいSLOに縛られて数%の劣化も許されないシステムと、短命なプロセスを大量に起動するバッチ基盤です。この2つは再現環境での取得に留めます。detailの常時有効化は、どの場合でも勧めません。メモリ調査を含めたJavaシステムの運用設計から相談したい場合は、フルスクラッチ開発の一環としてお受けしています。
JDKのバージョンで変わったNMTの仕様
NMTは改修が続いている領域で、手元のJDKによって使えるオプションも出力も違います。運用に影響する変更を並べます。
| JDK | 変更内容 | 課題番号 |
|---|---|---|
| 18(17.0.7) | VM.native_memory shutdownを削除 | JDK-8277946 |
| 20(17.0.10) | 無効時のコストを解消 | JDK-8296437 |
| 20 | JFRイベントを追加 | JDK-8157023 |
| 21 | MallocLimitの書式を刷新 | JDK-8293313 |
| 22(21.0.2) | peak値をリリースビルドでも表示 | JDK-8317772 |
| 22(21.0.4) | mmap行のpeak(at peak)表示 | JDK-8293850 |
| 22 | コールスタックにライブラリ名 | JDK-8319437 |
| 22(21.0.8) | jcmd System.map(Linux) | JDK-8318636 |
| 23 | statisticsがsummaryで動作 | JDK-8331858 |
| 24 | コールスタックにファイル名と行(デバッグ情報付きビルド) | JDK-8333994 |
| 26 | JNIカテゴリを追加 | JDK-8268406 |
| 26 | メモリ破壊の検出時に確保元を表示 | JDK-8369527 |
| 27 | JVMCIカテゴリが消滅 | JDK-8382582 |
Stack Overflowの回答や古い解説を参照するときは、前提のJDKを確かめてください。21.0.1以前や17.0.9以前ではpeakが出ないぶん、baselineとsummary.diffの重要度が上がります。JDK 27のその他の変更点はJava 27の変更点で、低遅延GCに切り替えたときのGC領域の違いはZGCの解説で扱っています。
よくある質問
NMTは本番環境で常時有効にしてよいですか?
summaryなら有効にしておくことを勧めます。公式の目安は性能5〜10%で、メモリ障害の原因を後から一切追えなくなるリスクと比べれば払う価値があります。レイテンシのSLOが厳しいシステムでは、再現環境での取得に切り替えてください。detailの常時有効化は勧めません。
-XX:NativeMemoryTracking=detail は起動後に切り替えられますか?
できません。NMTのモードは起動オプションで決まり、実行中のJVMに対して有効化・モード変更・停止はできません。停止用の shutdown もJDK 18とJDK 17.0.7で削除されています。detailが必要になったら、オプションを付けて再起動します。
NMTのcommitted合計とRSSが合わないのはなぜですか?
理由は2つあります。1つは、NMTがHotSpot VM自身の確保しか数えないことです。JNIやFFMのネイティブコード、JDKのクラスライブラリが直接mallocした領域は出ません。もう1つは、committedがコミットした量でRSSは物理メモリに載っている量と、定義が違うことです。実測では、何もしないプロセスでcommitted 101MBに対しRSS 41MBでした。
DirectByteBufferの使用量はNMTのどこに出ますか?
Otherに出ます。FFMの Arena で確保したメモリも同じくOtherです。JNIライブラリやFFMから呼んだCのコードが自分で malloc した領域は含まれないので、Otherが増えていないのにRSSが増える場合は jcmd System.map でNMTの外側を調べます。
レポートで最初に見るべきカテゴリはどこですか?
最初にTotalのcommittedとRSSを並べ、差が小さければ内訳へ進みます。内訳で問題になりやすいのはThread(スレッドの作りすぎ)、ClassとMetaspace(クラスローダのリーク)、Code(コードキャッシュの肥大)、Other(ダイレクトメモリ)の4つです。差が大きければNMTの外側を疑い、System.mapに移ります。