Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Java_プロセスのメモリ監視の落とし穴_NMT_で見抜けない_glibc_キャッシュ問題_.pdf

 Java_プロセスのメモリ監視の落とし穴_NMT_で見抜けない_glibc_キャッシュ問題_.pdf

2025年11月15日(土)に開催された JJUG CCC 2025 Fall にて発表した資料です

More Decks by NTTドコモソリューションズ Java担当

Other Decks in Programming

Transcript

  1. © NTT DOCOMO SOLUTIONS, Inc. 2025 1 スピーカー ◼ NTT

    ドコモソリューションズ(旧NTTコムウェア) ◼ 坂本 統(テックリード) ◼ 坂本 翔平(テックリード) ◼ Java/OpenJDK 関連技術調査、Java システムのトラブ ルシューティングをやってます ◼ JJUG CCC 発表実績 ◼ GraalVM Native Image 解析方法の紹介 ◼ Native Memory Tracking を使用した Java プロセ スのメモリ消費内訳の紹介 ◼ [New!]GraalVM Native Image トラブルシューティ ング機能の最新状況(2025年版) 坂本 統 (メイン) 坂本 翔平 (サブ)
  2. © NTT DOCOMO SOLUTIONS, Inc. 2025 2 本日の話 私たちが遭遇した Java

    プロセスのメモリリークらしき事象と、それを調査し て分かった原因、対処方法について紹介します ⚫ 問題発生と調査 ⚫ 発生した事象 ⚫ Native Memory Tracking 調査 ⚫ 既知バグ調査 ⚫ 原因 ⚫ JDK-8269345 概説 ⚫ C ヒープチェック ⚫ キャッシュ・トリム ⚫ 対処方法 ⚫ ワークアラウンド ⚫ まとめ
  3. © NTT DOCOMO SOLUTIONS, Inc. 2025 3 発生した事象 Kubernetes で動作する

    Java App でメモリリークらしき事象が発生 Kubernetes で動作する Java アプリケーションのメモリ消費量(working set)を監視していたところ、 起動から数日経過しても安定せず微増し続けた。本環境の Kubernetes は swap 領域がなく、また Pod のメモリサイズも制限していたことから OOM Killed が危ぶまれた。そこで Java プロセスのメモリリー クを想定して原因の調査を開始した。 Memory Usage Time Pod Mem Limit working set Pod のメモリ消費(イメージ) Day 1 Day 2 このままでは OOM Killed の危機 起動後からしばらくは メモリコミットで増加 数日経過しても 安定せず微増し続ける Working set https://ja.wikipedia.org/wiki/%E3%83%AF%E3%83%BC%E3%82%AD%E3%83%B3%E3%82%B0%E3%82%BB%E3%83%83%E3%83%88 Kubernetes - Resource metrics pipeline https://kubernetes.io/docs/tasks/debug/debug-cluster/resource-metrics-pipeline/#memory
  4. © NTT DOCOMO SOLUTIONS, Inc. 2025 4 Native Memory Tracking

    調査 NMT で JVM のメモリ消費内訳を確認してリーク箇所の特定を試みることに 本事象は Java プロセスのメモリ増加である。そこで原因を把握するため Native Memory Tracking(NMT) 機能を利用して、JVM のどの領域のメモリ消費が増加しているのかを確認することに。 しかし NMT の測定結果から、リーク傾向のある領域は確認できなかった。さらに不思議なことに NMT の Total(全体のメモリ消費)は安定し、かつ working set を数百 MB も下回る結果となった。 working set と NMT の数値は参照元情報が異なるため一致するものではないが、それでも大きな差が出 ていることに疑問が生じた。 (参考)NMT(Total) 出力例 Native Memory Tracking: … Total: reserved=xxxKB, committed=xxxKB malloc: xxxKB #xxx mmap: reserved=xxxKB, committed=xxxKB Java Heap Metaspace Others Code Thread (参考)NMT で測定可能なメモリ領域 Class GC GCCardS et Compiler Internal Other Symbol NMT Shared Class space Arena Chunk Tracing Logging Statistics Argumen ts Module Safepoint Synchron ization Servicea billity String Duplicati on Object Monitors Unknown
  5. © NTT DOCOMO SOLUTIONS, Inc. 2025 5 既知バグ調査 JDK Bug

    System で NMT と working set に差分が生じる事例を調査 JDK-8269345 の Problem で working set の増加と NMT で確認できない問題の記述を発見した。 JDK-8269345: Add Linux-specific jcmd to trim the C-haep https://bugs.openjdk.org/browse/JDK-8269345
  6. © NTT DOCOMO SOLUTIONS, Inc. 2025 6 JDK-8269345 概説 glibc

    は C ヒープの解放されたメモリを OS に返却せずキャッシュとして持ち 続ける特性があり working set が肥大化しやすい Java Process JVM は malloc/mmap でメモリを確保し、不要 になれば free で C ヒープへ返却する。この時、 多くの libc 実装はその一部を OS へ返却するが、 glibc は返却に消極的でキャッシュとして保持す る悪名高い性質がある。特に小さな割当・解放を 頻繁に行うとキャッシュの必要性が高まることで この性質が顕著となり、結果として working set や rss が永続的に増加する。 JVM の NMT は malloc/mmap を追跡するた め、この glibc のキャッシュは参照できない。そ のため NMT の Total と working set/rss に大き な差ができてしまう。 C-heap Operating System JVM NMT retained (cache) 1.free by JVM 2.cache by glibc freed 2.release by other libc allocated working set glibc のキャッシュ保持の様子(イメージ)
  7. © NTT DOCOMO SOLUTIONS, Inc. 2025 7 C ヒープチェック VM.info

    からキャッシュ増大を確認 VM.info の Process Memory から glibc の C ヒープの割り当て(outstanding allocations)と キャッシュ(retained)を確認できる。実際に実行中の Java プロセスの情報を取得したところ、 retained が outstanding を大きく上回って数百 MB 保持していた。 ※ Process Memory の数値は、メモリ割り当て情報を取得する mallinfo() から参照している (参考)mallinfo(3) https://man7.org/linux/man-pages/man3/mallinfo.3.html (参考)JDK 実装箇所抜粋 https://github.com/openjdk/jdk25u/blob/master/src/hotspot/os/linux/os_linux.cpp#L2439 $ jcmd <pid> VM.info … Process Memory: Virtual Size: xxxxxK (peak: xxxxxK) Resident Set Size: xxxxxK (peak: xxxxxK) (anon: xxxxxK, file: xxxxxK, shmem: 0K) Swapped out: 0K C-Heap outstanding allocations: xxxxxK, retained: xxxxxK glibc malloc tunables: (default) … Process Memory 出力例
  8. © NTT DOCOMO SOLUTIONS, Inc. 2025 8 キャッシュ・トリム System.trim_native_heap で

    working_set 減少 前述の JDK-8269345 はこのキャッシュ問題に対処する機能改善である。glibc は free されたメモリを OS に返却する API の malloc_trim() を備える。JDK-8269345 によって JVM の内部でこの API を呼び 出すコマンドが実装された。jcmd で System.trim_native_heap を実行すると内部的に malloc_trim() が実行され、Java プロセスの glibc キャッシュをトリミングして OS に返却できる。 実際に問題の Java プロセスに対してこのコマンドを実行すると RSS と working_set が大きく数百 MB 減少したため、本事象に該当していたと確証を得た。 $ jcmd <pid> System.trim_native_heap <pid>: Attempting trim... Done. Virtual size before: xxxxxk, after: xxxxxk, (-xxxxxk) RSS before: xxxxxk, after: xxxxxk, (-xxxxxk) Swap before: 0k, after: 0k, (0k) System.trim_native_heap 実行例
  9. © NTT DOCOMO SOLUTIONS, Inc. 2025 9 (参考)OpenJDK 実装の抜粋 実装上は

    malloc_trim() を呼び出すだけ(Linux + glibc のみ) bool os::trim_native_heap(os::size_change_t* rss_change) { #ifdef __GLIBC__ os::Linux::meminfo_t info1; os::Linux::meminfo_t info2; bool have_info1 = rss_change != nullptr && os::Linux::query_process_memory_info(&info1); ::malloc_trim(0); bool have_info2 = rss_change != nullptr && have_info1 && os::Linux::query_process_memory_info(&info2); ssize_t delta = (ssize_t) -1; if (rss_change != nullptr) { if (have_info1 && have_info2 && info1.vmrss != -1 && info2.vmrss != -1 && info1.vmswap != -1 && info2.vmswap != -1) { // Note: query_process_memory_info returns values in K rss_change->before = (info1.vmrss + info1.vmswap) * K; rss_change->after = (info2.vmrss + info2.vmswap) * K; } else { rss_change->after = rss_change->before = SIZE_MAX; } } JDK 実装箇所抜粋 https://github.com/openjdk/jdk25u/blob/master/src/hotspot/os/linux/os_linux.cpp#L5506
  10. © NTT DOCOMO SOLUTIONS, Inc. 2025 10 ワークアラウンド System.trim_native_heap or/and

    -XX:TrimNativeHeapInterval JVM は以下の2つのトリム手段を提供する。 • jcmd System.trim_native_heap:トリムを実行するコマンド ※1 • -XX:TrimNativeHeapInterval:指定した間隔(ms)でトリムを実行するオプション ※2 また glibc 側の挙動をチューニングする方法もある(詳細は割愛)※3 ただしキャッシュ・トリムは最適な手段ではないことに注意すること。本来キャッシュはメモリの確保を 効率化する手段であり、トリムした後でメモリを再取得することがあれば、それが余計なコストとなって 性能に影響が出る可能性がある。また glibc も無制限にキャッシュをため続けることはなく必要に応じて 削除するため、トリムしないからといって OOM Killed されるとは限らない。 JVM のキャッシュ・トリム機能は自己責任での実行を前提として追加された経緯がある。他の要因による メモリリーク解析やメモリ監視のためのノイズ除去、あるいはアプリケーションの特性でトリムが有効で あることを確認した後など、明確な目的がある場合にのみ実行することを推奨する。 ※1 11.0.18, 17.0.2 以降で利用可能(https://bugs.openjdk.org/browse/JDK-8269345) ※2 17.0.9, 21.0.1 以降でエクスペリメンタル機能として利用可能(https://bugs.openjdk.org/browse/JDK-8293114) 17.0.12, 21.0.3 以降はプロダクション機能として利用可能(https://bugs.openjdk.org/browse/JDK-8325496) ※3 mallopt(3) - M_TRIM_THREDHOLD (https://man7.org/linux/man-pages/man3/mallopt.3.html)
  11. © NTT DOCOMO SOLUTIONS, Inc. 2025 11 まとめ working set

    や rss が肥大化する時は glibc キャッシュが原因の可能性あり • glibc は free されたメモリを OS に返却せず C ヒープにキャッシュとして保持する性質がある • glibc キャッシュが増えることで working set や rss で見たメモリ消費が肥大化する • Native Memory Tracking は glibc キャッシュを参照できない • VM.info の Process Memory で C ヒープのキャッシュサイズを確認できる • glibc が実装する malloc_trim() API でキャッシュをトリミングできる • JVM は内部的に malloc_trim() を実行する手段を2つ提供する • System.trim_native_heap:1回キャッシュ・トリムを実行 • -XX:TrimNativeHeapInterval:指定した間隔で繰り返しキャッシュ・トリムを実行 • キャッシュ・トリムは自己責任で • トリムによる性能影響が発生する可能性あり • glibc キャッシュによって OOM Killed にはならない(はず) • 明確に必要であることが確認できた場合にキャッシュ・トリムを実行することを推奨