Javaパフォーマンス問題を徹底解析|ヒープダンプ・スレッドダンプ・コアダンプの取得と解析方法

はじめに

Javaアプリケーションを運用していると、突然パフォーマンスが低下したり、応答が遅くなるといった問題に直面することがあります。こうしたトラブルの原因を特定するには、アプリケーションの内部状態を記録したダンプ(ヒープダンプ・スレッドダンプ・コアダンプ)を取得して分析することが有効です。本記事では、それぞれのダンプの役割や取得方法、そしてパフォーマンス問題のトラブルシューティングにおける活用法を、具体例を交えて解説します。

各種ダンプの比較

各種ダンプにはそれぞれ用途と特徴があります。
調査の際は、まず取得時のサーバに対する負荷が小さい情報から取得するのが基本です。

  • 軽量に状態を確認したい場合はスレッドダンプ
  • メモリ使用量やリークが疑われる場合はヒープダンプ
  • これらでは原因が特定できない場合や JVM/ネイティブレベルの深刻な問題が疑われる場合は、最終手段としてコアダンプ

という方針で、状況に応じて適切なダンプを取得してください。以下に詳細を記載します。

項目スレッドダンプ (Thread Dump)ヒープダンプ
(Heap Dump)
コアダンプ
(Core Dump)
取得目的スレッドの状態やデッドロック調査Javaヒープ領域の内容を解析するためプロセス全体のメモリ状態を解析(クラッシュ原因調査)
取得内容各スレッドの状態、スタックトレースオブジェクト、クラス、ヒープ使用状況プロセス全体のメモリ、レジスタ、スタック等
取得タイミング任意のタイミングで取得可能任意のタイミングで取得可能異常終了時 or 手動で強制取得
役立つ場面ハング、デッドロック、スレッド競合調査メモリリーク、OutOfMemoryError調査OSレベルのクラッシュ、JVMバグ解析
サイズ小さい(数KB〜数MB)大きい(数百MB〜GB)非常に大きい(GB級)
取得が与える負荷低い高い(GC停止やアプリ停止が発生)非常に高い(プロセス停止)
取得方法kill -3、jstack、jcmdjmap、jcmdcore ファイルを OS が生成、または gcore
解析ツールJDK付属ツール(JConsole)、VisualVMEclipse MAT、VisualVMgdb、lldb、jhsdb
アプリ継続性継続可能基本的に継続可能(ただし一瞬重くなる)停止(クラッシュ扱い)

スレッドダンプ(Thread Dump)の取得と解析方法

スレッドダンプとは、Java仮想マシン(JVM)内で動作している全スレッドの情報をまとめて出力したものです。主に以下の情報を確認できます。

  • 各スレッドの状態(RUNNABLE、BLOCKED、WAITING、TIMED_WAITINGなど)
  • スレッドが保持しているロックや待機しているロック
  • スタックトレース(どのメソッドで停止しているか)
  • デッドロックの有無

これらを分析することで、アプリケーションのパフォーマンスボトルネック、デッドロック、メモリリークの原因などを特定できます。

スレッドダンプの取得方法

killコマンドによるスレッドダンプの取得方法

killコマンドにオプション -3 を付けることで、スレッドダンプを取得することができます。OS標準のコマンドなので、どんな環境でも情報取得が可能です。また、スレッドが固まっていても情報を取得することが可能です。パフォーマンスに対する影響も少ないので、本番環境では本コマンドによるスレッドダンプ取得を推奨します。
試しに、tomcatのプロセスに対して kill -3 コマンドを実行してみましょう。catalina.outログにスレッドダンプの情報が出力されます。

[root@quiz ~]# kill -3 274968
[root@quiz ~]# tail -1000 /opt/tomcat/logs/catalina.out
~~Truncated~~
2025-12-02 06:29:22
Full thread dump OpenJDK 64-Bit Server VM (21.0.2+13-58 mixed mode, sharing):
Threads class SMR info:
_java_thread_list=0x00007f366c002460, length=31, elements={
0x00007f371c02ce30, 0x00007f371c0ecff0, 0x00007f371c0ee650, 0x00007f371c0f0140,
0x00007f371c0f1790, 0x00007f371c0f2d40, 0x00007f371c0f4890, 0x00007f371c0f5f60,
0x00007f371c11f7b0, 0x00007f371c17e380, 0x00007f371c1f9610, 0x00007f371c2c3210,
0x00007f371c2cda70, 0x00007f371c2d5d30, 0x00007f371c568c80, 0x00007f371db6a490,
0x00007f371db63890, 0x00007f371e9bfce0, 0x00007f3648001200, 0x00007f371c9da6a0,
0x00007f371c9dba80, 0x00007f371c9dcd20, 0x00007f371c9de3b0, 0x00007f371ca00e60,
0x00007f371ca02000, 0x00007f371ca036b0, 0x00007f371ca04d60, 0x00007f371ca06330,
0x00007f371ca07900, 0x00007f371c9f9b70, 0x00007f371c9850b0
}
"main" #1 [274969] prio=5 os_prio=0 cpu=12421.73ms elapsed=108.91s tid=0x00007f371c02ce30 nid=274969 runnable  [0x00007f3723710000]
   java.lang.Thread.State: RUNNABLE
        at sun.nio.ch.Net.accept(java.base@21.0.2/Native Method)
        at sun.nio.ch.NioSocketImpl.accept(java.base@21.0.2/NioSocketImpl.java:748)
        at java.net.ServerSocket.implAccept(java.base@21.0.2/ServerSocket.java:698)
        at java.net.ServerSocket.platformImplAccept(java.base@21.0.2/ServerSocket.java:663)
        at java.net.ServerSocket.implAccept(java.base@21.0.2/ServerSocket.java:639)
        at java.net.ServerSocket.implAccept(java.base@21.0.2/ServerSocket.java:585)
        at java.net.ServerSocket.accept(java.base@21.0.2/ServerSocket.java:543)
        at org.apache.catalina.core.StandardServer.await(StandardServer.java:557)
        at org.apache.catalina.startup.Catalina.await(Catalina.java:851)
        at org.apache.catalina.startup.Catalina.start(Catalina.java:799)
~~Truncated~~
"G1 Conc#0" os_prio=0 cpu=203.73ms elapsed=108.91s tid=0x00007f371c06e090 nid=274972 runnable
"G1 Refine#0" os_prio=0 cpu=136.13ms elapsed=108.90s tid=0x00007f371c0b08e0 nid=274973 runnable
"G1 Service" os_prio=0 cpu=7.07ms elapsed=108.90s tid=0x00007f371c0b18a0 nid=274974 runnable
"VM Periodic Task Thread" os_prio=0 cpu=96.28ms elapsed=108.88s tid=0x00007f371c0c2930 nid=274975 waiting on condition
JNI global refs: 21, weak refs: 0
Heap
 garbage-first heap   total 98304K, used 76135K [0x00000000c6e00000, 0x0000000100000000)
  region size 1024K, 45 young (46080K), 7 survivors (7168K)
 Metaspace       used 73266K, committed 74048K, reserved 1114112K
  class space    used 9706K, committed 10048K, reserved 1048576K

jstackによるスレッドダンプの取得方法

JDKがインストールされている場合には、JDKに同梱されている jstack というツールを使ってスレッドダンプを取得することができます。下記の通りコマンドを実行することで、ロック情報を含めてファイルにスレッドダンプを出力することができます。

[root@quiz ~]# jstack -l 1022886 > /tmp/threaddump_$(date +%Y%m%d_%H%M%S).txt
[root@quiz ~]#

1022886 はスレッドダンプ取得対象のPIDを指定しています。psコマンド等でPIDを確認してからコマンドを実行してください。l オプションを付けると、スレッドが保持しているロック情報も取得可能です。また、一度だけの取得ではなく、時間を空けて何回か取得して比較することで、スレッド使用状況の傾向やボトルネックを分析できます。

jconsole / VisualVM よるスレッドダンプの取得方法

JMX(Java Management Extensions)を有効化している場合、jconsole や VisualVM でスレッドダンプをGUI上で取得可能です。

スレッドダンプ取得時の注意点

スレッドダンプの取得はヒープダンプやコアダンプに比べて安全ですが、商用運転中のシステムで実行する場合にはいくつかの注意が必要です。以下の点を理解したうえで、取得タイミングを判断してください。

  1. アプリケーションが一瞬停止する可能性があります
    スレッドダンプ取得中、JVM 全体は停止しませんが、一時的に Stop-The-World が発生し、アプリケーションスレッドが一瞬停止することがあります。また、取得時に CPU や I/O の負荷が一時的に増加する場合があります。結果としてスループット低下やレスポンス遅延が発生する可能性があるため、負荷の低い時間帯に実行することを推奨します。
  2. 複数回の取得が必要です
    スレッドの状態は瞬間的に変化するため、1回のスレッドダンプだけでは問題を特定できないことが多くあります。5回以上のスレッドダンプを、数秒間隔で取得することを推奨します。
  3. 出力ファイルサイズに注意してください
    JVM やアプリケーションが多数のスレッドを持つ場合、スレッドダンプの出力サイズが大きくなることがあります。十分な空き容量がある、かつサービス影響が発生しない専用のログ領域へ出力することを推奨します。

スレッドダンプの解析方法

一例として、jstackコマンドで取得したスレッドダンプを解析してみましょう。
下記は、非常に分かりやすくデッドロックが検出されている例となります。Thread-0 と Thread-1 がお互いにロックを取り合ってデッドロックになっていることが見て取れます。

2025-12-02 06:42:26
Full thread dump OpenJDK 64-Bit Server VM (21.0.2+13-58 mixed mode, sharing):
~~Truncated~~
Found one Java-level deadlock:
=============================
"Thread-0":
  waiting to lock monitor 0x00007fa568002d90 (object 0x00000000ca711b58, a java.lang.Object),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock monitor 0x00007fa55c001160 (object 0x00000000ca711b48, a java.lang.Object),
  which is held by "Thread-0"
Java stack information for the threads listed above:
===================================================
"Thread-0":
        at DeadlockSample.lambda$main$0(DeadlockSample.java:16)
        - waiting to lock <0x00000000ca711b58> (a java.lang.Object)
        - locked <0x00000000ca711b48> (a java.lang.Object)
        at DeadlockSample$$Lambda/0x00007fa598000a00.run(Unknown Source)
        at java.lang.Thread.runWith(java.base@21.0.2/Thread.java:1596)
        at java.lang.Thread.run(java.base@21.0.2/Thread.java:1583)
"Thread-1":
        at DeadlockSample.lambda$main$1(DeadlockSample.java:29)
        - waiting to lock <0x00000000ca711b48> (a java.lang.Object)
        - locked <0x00000000ca711b58> (a java.lang.Object)
        at DeadlockSample$$Lambda/0x00007fa598000c18.run(Unknown Source)
        at java.lang.Thread.runWith(java.base@21.0.2/Thread.java:1596)
        at java.lang.Thread.run(java.base@21.0.2/Thread.java:1583)
Found 1 deadlock.

ヒープダンプの取得と解析方法

ヒープダンプ(Heap Dump)とは、JVMのヒープ領域のメモリ内容をファイルとして出力したものです。メモリリークの原因特定や、オブジェクトを過剰保持していないかといった観点で解析が可能です。

ヒープダンプの取得方法

jcmdコマンドによるヒープダンプ取得方法

本番環境にて稼働中にヒープダンプを取得する場合は、jcmdコマンドを実行することによって安全に情報を取得することができます。以下の通りにコマンドを実行することで、ヒープダンプ(hprofファイル)を取得することができます。

[root@quiz ~]# jcmd 311127 GC.heap_dump /tmp/heapdump_$(date +%Y%m%d_%H%M%S).hprof
311127:
Dumping heap to /tmp/heapdump_20251203_161828.hprof ...
Heap dump file created [67250434 bytes in 0.793 secs]
[root@quiz ~]#

OOM発生時のヒープダンプ自動取得方法

Javaアプリケーションが OOM(OutOfMemory)で停止した場合、自動でダンプを出力させることもできます。tomcat における設定例を記載します。setenv.sh に下記の設定を追加して下さい。
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_BASE/logs/heapdump_$(date +%Y%m%d_%H%M%S).hprof”

[root@quiz ~]# cat /opt/tomcat/bin/setenv.sh
export JAVA_HOME=/opt/java/jdk-21.0.2
export JRE_HOME=$JAVA_HOME
export PATH=$JAVA_HOME/bin:$PATH
# heap dump setting
CATALINA_OPTS="$CATALINA_OPTS \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump_$(date +%Y%m%d_%H%M%S).hprof"
[root@quiz ~]#
[root@quiz ~]# systemctl restart tomcat
[root@quiz ~]#

jmapコマンドによるヒープダンプ取得方法

jmap(Java Memory Map)コマンドを用いることで、実行中の Java プロセスの メモリ使用状況を確認や、ヒープダンプを取得することができます。しかし、環境によってJVMがハングして落ちてしまうケースも報告されているので、本コマンドの実行は控えてjcmdコマンドを使用することを推奨します。
以下の通り、jmapコマンドでヒープダンプ(hprofファイル)を取得することができます。

[root@quiz ~]# jmap -dump:format=b,file=/tmp/heapdump_$(date +%Y%m%d_%H%M%S).hprof 996008
Dumping heap to /tmp/heapdump_20251112_063454.hprof ...
Heap dump file created [126793933 bytes in 1.171 secs]
[root@quiz ~]#

コマンドのオプションの意味は以下の通りです。

  • -dump
    ヒープ全体の内容をダンプ(書き出す)指定。
  • format=b
    バイナリ形式 で出力。
  • file=
    出力するファイル名・パスを指定。書き込み権限のあるディレクトリを指定してください。
  • 996008
    解析対象のPIDを指定してください。psコマンド等で確認することが可能です。

ヒープダンプ取得時の注意点

ヒープダンプの取得は JVM のメモリ状態を詳細に把握できる有効な手法ですが、商用運転中のシステムで実行する場合には大きなリスクを伴います。以下の点から、基本的には商用システム運転中の取得は推奨できません。どうしても必要な場合は、サービス停止時間帯、一時停止が許容できるタイミング、負荷の低い時間帯での取得を推奨します。

  1. アプリケーションが停止する場合があります(Stop The World)
    ヒープダンプ取得では JVM 全体のメモリをスキャンするため、多くの JVM で Stop-The-World が発生します。ヒープサイズが大きい(例:8GB 以上)、オブジェクト数が多い、メモリリーク発生中などの場合、数秒〜数十秒以上の停止が起こる場合があります。また、取得方法によっては JVM が強制終了 するものもあるため、使用するコマンドの性質を事前に確認してください。
  2. パフォーマンス劣化やタイムアウトのトリガーになる場合があります
    ヒープダンプ取得中は、スレッドのブロッキング増加、I/O遅延、外部API・DBアクセス遅延などのによりパフォーマンスに影響する可能性があります。
  3. 取得後のファイルサイズが非常に大きくなる可能性があります
    ヒープダンプは JVM のヒープ全体を書き出すため、数GB〜数十GBになることがあります。ファイルサイズが大きすぎると、ディスクフルによるアプリケーション停止、I/O 負荷の急増によるレスポンス遅延、書き込みが遅くなり Stop The World 時間がさらに延びるなどの影響が考えられます。十分な空き容量がある、かつサービス影響が発生しない専用のログ領域へ出力することを推奨します。

ヒープダンプの解析方法

ヒープダンプの解析には、Eclipse Memory Analyzer (MAT) を使うのが一般的です。クライアント端末において、下記のEclipseのサイトからダウンロードしてzipを解凍し、MemoryAnalyzer.exe を実行して起動してください。
Downloads | The Eclipse Foundation

以下のようなGUI画面が表示されます。

File > Open File… をクリックして、出力したダンプファイル(hprof)を選択します。

Leak Suspects Report を選択して Finish を押下してください。

MATにはいろいろ機能がありますが、下記のような流れでリークの原因を特定していくのが良いでしょう。

① Suspects Report(概要把握および怪しい箇所の特定)
② Histogram(メモリを消費しているクラスの特定)
③ Dominator Tree(何がクラスを保持しているか、原因を特定)
④ Path to GC Roots(参照チェーンを確定し、原因を特定)
それぞれ説明していきます。

Suspects Report(概要把握および怪しい箇所の特定)

hprof を開くとLeak Suspects レポートが出力されますので、まずは全体の概要を俯瞰して把握し、異常なオブジェクトや巨大なコレクションの存在を把握します。
今回はサンプルでMemoryLeakDemo というプログラムを実行しており、分かりやすくヒープを占領していることが見て取れます。

Histogram(メモリを消費しているクラスの特定)

次に、クラス単位でオブジェクト数、Shallow Heap、Retained Heap を確認し、増え続けているクラスを特定します。

Overview > Histogram を押下します。

以下の通り、Class Name の一覧が出力されます。

各項目の意味は以下となります。

  • Objects
    クラスのインスタンス(オブジェクト)の数を示します。オブジェクト数が極端に多いクラスを探しましょう。
  • Shallow Heap
    オブジェクト自身がヒープ上で消費しているメモリ量(バイト数)を示します。参照先オブジェクトのサイズは含まれません。
  • Retained Heap
    オブジェクトが保持している(参照している)オブジェクトも含めた、依存関係全体のメモリ量(バイト数)となります。対象のオブジェクトがガベージコレクションで解放されると、一緒に消えるオブジェクト群のメモリ量の合計サイズとなります。Retained Heap が大きいオブジェクトがあった場合、メモリリークしている可能性がありますので確認してみてください。

続いて、Retained Heap が大きいクラスを見つけたら、詳細を確認していきましょう。
Class Name を右クリック > List Objects > with incoming references を押下します。

incoming references とは対象のオブジェクトを参照している参照元のオブジェクトとなります。何のオブジェクトから参照されているかを確認して、メモリリークの原因を特定しましょう。

Dominator Tree(何がクラスを保持しているか、原因を特定)

Retained Heap が大きいクラスを確認することで、解放できないメモリを大量に保持している原因を特定していきましょう。本ケースでは、分かりやすくMemoryLeakDemoクラスが大量のメモリを保持していることが見て取れます。

Path to GC Roots(参照チェーンを確定し、原因を特定)

Dominator Treeで怪しいクラスを見つけたら、Path to GC Roots(GCルートへのパス)を確認してみてください。なぜGCできないか原因を特定できます。
特定のクラスを右クリック > Path to GC Roots > exclude weak/soft references を選択します。

elementData java.util.ArrayList @ 0xdff1e878 が確認できることから、下記事象が起きていることが分かります。

  • MemoryLeakDemo 内の ArrayList が大量のオブジェクトを保持
  • ArrayList.elementData[] が巨大な配列
  • 上記配列が GC で解放されず、メモリリークの根本原因

コアダンプの取得方法と解析方法

スレッドダンプ、ヒープダンプでも原因が特定できない場合は、最終手段としてコアダンプを取得してみてください。あるいは、JVMが停止してしまった場合は自動でコアダンプを取得する設定にしておくと良いでしょう。ここでは tomcat の例でご説明します。

コアダンプの取得方法

gcoreコマンドによるコアダンプ取得方法

gcoreコマンドを実行することにより、JVMのプロセスを停止することなくコアダンプを取得することが可能です。

[root@quiz ~]# gcore -o /var/tomcat 311127
[New LWP 311252]
[New LWP 311188]
~~Truncated~~
[New LWP 311129]
[New LWP 311128]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
0x00007f1d8788717a in __futex_abstimed_wait_common () from /lib64/libc.so.6
warning: Memory read failed for corefile section, 4096 bytes at 0xffffffffff600000.
Saved corefile /var/tomcat.311127
[Inferior 1 (process 311127) detached]
[root@quiz ~]#

JVM異常終了時のコアダンプ自動取得方法

JVMが異常停止した場合、コアダンプを出力するように設定しておくと解析に役立ちます。コアダンプを出力できるように、設定を追加しておきましょう。tomcat の起動オプションに下記の設定を追加してください。

[root@quiz ~]# vi /etc/systemd/system/tomcat.service
[root@quiz ~]# cat /etc/systemd/system/tomcat.service
[Unit]
Description=Apache Tomcat 10
After=network.target
~~Truncated~~
##Core Dump Setting
LimitCORE=infinity
~~Truncated~~
[root@quiz ~]#
[root@quiz ~]# systemctl daemon-reload
[root@quiz ~]# systemctl restart tomcat
[root@quiz ~]#

それではコアダンプを取得してみましょう。tomcatのプロセスIDを指定して kill -SEGV コマンドを実行して少し待つと、コアダンプが取得されていることを確認できるはずです。

[root@quiz ~]# kill -SEGV 300774
[root@quiz ~]#
[root@quiz ~]# coredumpctl list
No coredumps found.
-- Notice: 1 systemd-coredump@.service unit is running, output may be incomplete.
[root@quiz ~]# coredumpctl list
TIME                           PID UID GID SIG     COREFILE EXE                             SIZE
Wed 2025-12-03 06:56:52 JST 300774 996 993 SIGABRT present  /opt/java/jdk-21.0.2/bin/java 107.7M
[root@quiz ~]#

続いてコアダンプをファイルに出力させましょう。

[root@quiz ~]# coredumpctl dump 300774 > /tmp/core-300774
           PID: 300774 (java)
           UID: 996 (tomcat)
           GID: 993 (tomcat)
        Signal: 6 (ABRT)
     Timestamp: Wed 2025-12-03 06:56:44 JST (1min 58s ago)
  Command Line: /opt/java/jdk-21.0.2/bin/java -Djava.util.logging.config.file=/opt/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dsun.io.useCanonCaches=false -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9011 -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.access.file=/opt/tomcat/conf/jmxremote.access -Dcom.sun.management.jmxremote.password.file=/opt/tomcat/conf/jmxremote.password -Dcom.sun.management.jmxremote.registry.ssl=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=false -Djava.rmi.server.hostname=quiz.eeengineer.com $'-Xlog:gc*,safepoint:file=/opt/tomcat/logs/gc.log:time,level,tags:filecount=5,filesize=10M' -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump_20251203_064958.hprof -classpath /opt/tomcat/bin/bootstrap.jar:/opt/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/opt/tomcat -Dcatalina.home=/opt/tomcat -Djava.io.tmpdir=/opt/tomcat/temp org.apache.catalina.startup.Bootstrap start
    Executable: /opt/java/jdk-21.0.2/bin/java
 Control Group: /system.slice/tomcat.service
          Unit: tomcat.service
         Slice: system.slice
       Boot ID: f945e8fb8a004dc0ba4b133ac7c2252c
    Machine ID: 49a9003f8f0e4a25a5bf4d147e21e0d4
      Hostname: quiz.eeengineer.com
       Storage: /var/lib/systemd/coredump/core.java.996.f945e8fb8a004dc0ba4b133ac7c2252c.300774.1764712604000000.zst (present)
  Size on Disk: 107.7M
       Message: Process 300774 (java) of user 996 dumped core.
                Module libverify.so without build-id.
                Module libextnet.so without build-id.
                Module libmanagement_ext.so without build-id.
                Module libmanagement.so without build-id.
                
 ~~Truncated~~
          #16 0x00007fdedc029680 __libc_start_main@@GLIBC_2.34 (libc.so.6 + 0x29680)
                #17 0x0000564ffc718b75 _start (java + 0xb75)
                ELF object binary architecture: AMD x86-64
[root@quiz ~]#
[root@quiz ~]# ll /tmp/core-300774
-rw-r--r-- 1 root root 585781248 Dec  3 06:58 /tmp/core-300774
[root@quiz ~]#

コアダンプ取得時の注意点

コアダンプの取得はスレッドダンプやヒープダンプに比べてプロセス全体のメモリ状態を取得できるため有効ではありますが、以下の点から、基本的には商用システム運転中の取得は推奨できません。どうしても必要な場合は、サービス停止時間帯、一時停止が許容できるタイミング、負荷の低い時間帯での取得を推奨します。

  1. 取得中にシステム負荷が増加する可能性があります
    gcore を使用してプロセスを停止せずにコアダンプを取得する場合でも、取得中は JVM の全メモリをコピーするため CPU と I/O 負荷が一時的に増加します。特に大規模 JVM(ヒープサイズが数 GB 以上)の場合は影響が顕著になります。
  2. 取得後のファイルサイズが非常に大きくなる可能性があります
    コアダンプは JVM の全メモリ領域を含むため、ヒープダンプよりも大きなサイズになることがあります。ディスク容量を十分に確保し、他のサービスに影響を与えない専用ディレクトリへ出力することを推奨します。

コアダンプの解析方法

gdbコマンドによる解析方法

gcoreコマンドにて取得したコアダンプファイルは、gdb コマンドによって解析が可能です。gdb は GNU Debugger の略で、主に Linux や UNIX 系で使われる ネイティブプログラムのデバッグツールです。

[root@quiz ~]# gdb /opt/java/jdk-21.0.2/bin/java /var/tomcat.311127
GNU gdb (AlmaLinux) 16.3-2.el9
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
~~Truncated~~
[New LWP 311129]
[New LWP 311128]
This GDB supports auto-downloading debuginfo from the following URLs:
  <%{dist_debuginfod_url}>
Enable debuginfod for this session? (y or [n])
Debuginfod has been disabled.
--Type <RET> for more, q to quit, c to continue without paging--
To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Core was generated by `/opt/java/jdk-21.0.2/bin/java -Djava.util.logging.config.file=/opt/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dsun.io.useCanonCaches=false -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.rmi.port=9011 -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.access.file=/opt/tomcat/conf/jmxremote.access -Dcom.sun.management.jmxremote.password.file=/opt/tomcat/conf/jmxremote.password -Dcom.sun.management.jmxremote.registry.ssl=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.local.only=false -Djava.rmi.server.hostname=quiz.eeengineer.com -Xlog:gc\*,safepoint:file=/opt/tomcat/logs/gc.log:time,level,tags:filecount=5,filesize=10M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump_20251203_161446.hprof -classpath /opt/tomcat/bin/bootstrap.jar:/opt/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/opt/tomcat -Dcatalina.home=/opt/tomcat -Djava.io.tmpdir=/opt/tomcat/temp org.apache.catalina.startup.Bootstrap start'.
#0  0x00007f1d8788717a in __futex_abstimed_wait_common () from /lib64/libc.so.6
[Current thread is 1 (Thread 0x7f1d87ae4200 (LWP 311127))]
Missing rpms, try: dnf --enablerepo='*debug*' install zlib-debuginfo-1.2.11-40.el9.x86_64 glibc-debuginfo-2.34-168.el9_6.14.alma.1.x86_64 sssd-client-debuginfo-2.9.6-4.el9_6.2.x86_64
(gdb)

(gdb) が表示されたら、対話方式でコマンドを入力して必要な情報を取得します。よく使うコマンドは以下となります。

  • bt:バックトレース(クラッシュ時の関数呼び出し履歴)を表示
  • info threads:プロセス内のスレッド一覧を表示
  • thread <番号>:指定スレッドに切り替え
  • list:ソースコードを表示
  • print <変数>:変数の値を確認
  • quit:gdb を終了
(gdb) info thread
  Id   Target Id                          Frame
* 1    Thread 0x7f1d87ae4200 (LWP 311127) 0x00007f1d8788717a in __futex_abstimed_wait_common () from /lib64/libc.so.6
  2    Thread 0x7f1d599f9640 (LWP 311252) 0x00007f1d8790f91f in accept () from 
  ~~Truncated~~
 42   Thread 0x7f1d8494b640 (LWP 311129) 0x00007f1d8788717a in __futex_abstimed_wait_common () from /lib64/libc.so.6
  43   Thread 0x7f1d861ff640 (LWP 311128) 0x00007f1d8790f91f in accept () from /lib64/libc.so.6
(gdb)
(gdb) bt
#0  0x00007f1d8788717a in __futex_abstimed_wait_common () from /lib64/libc.so.6
#1  0x00007f1d8788bbf4 in __pthread_clockjoin_ex () from /lib64/libc.so.6
#2  0x00007f1d87af7a2f in CallJavaMainInNewThread () from /opt/java/jdk-21.0.2/bin/../lib/libjli.so
#3  0x00007f1d87af4bbd in ContinueInNewThread () from /opt/java/jdk-21.0.2/bin/../lib/libjli.so
#4  0x00007f1d87af560d in JLI_Launch () from /opt/java/jdk-21.0.2/bin/../lib/libjli.so
#5  0x0000556d450f7adf in main ()
(gdb)

参考

試験的にパフォーマンス問題を引き起こすサンプルプログラムを共有します。

メモリリークを起こすJavaプログラム

学習用に意図的にメモリリークを起こすJavaプログラムを記載します。実際に手を動かして確認したい方は、ぜひ試してみて下さい。

import java.util.ArrayList;
import java.util.List;
public class MemoryLeakDemo {
    private static final List<Object> leakList = new ArrayList<>();
    public static void main(String[] args) {
        System.out.println("Starting MemoryLeakDemo...");
        try {
            while (true) {
                // 1MBのbyte配列を生成してリストに追加
                byte[] data = new byte[1024 * 1024];
                leakList.add(data);
                // 現在のリストサイズを出力
                System.out.println("Objects in list: " + leakList.size());
                // 少し待つ(CPU負荷軽減)
                Thread.sleep(100);
            }
        } catch (OutOfMemoryError e) {
            System.err.println("OutOfMemoryError発生!");
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上記ファイルをMemoryLeakDemo.java という名称で作成し、下記の通り実行してください。

[root@quiz tmp]# vi MemoryLeakDemo.java
[root@quiz tmp]# cat MemoryLeakDemo.java
import java.util.ArrayList;
import java.util.List;
public class MemoryLeakDemo {
    private static final List<Object> leakList = new ArrayList<>();
    public static void main(String[] args) {
        System.out.println("Starting MemoryLeakDemo...");
        try {
            while (true) {
                byte[] data = new byte[1024 * 1024];
                leakList.add(data);
                System.out.println("Objects in list: " + leakList.size());
                Thread.sleep(100);
            }
        } catch (OutOfMemoryError e) {
            System.err.println("OutOfMemoryError発生!");
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
[root@quiz tmp]#
[root@quiz tmp]# javac MemoryLeakDemo.java
[root@quiz tmp]# java MemoryLeakDemo
Starting MemoryLeakDemo...
Objects in list: 1
Objects in list: 2
Objects in list: 3
Objects in list: 4
Objects in list: 5

デッドロックを起こすJavaプログラム

[root@quiz ~]# cd /tmp/
[root@quiz tmp]# vi /tmp/DeadlockSample.java
[root@quiz tmp]# cat /tmp/DeadlockSample.java
/**
 * This program intentionally causes a deadlock between two threads.
 * It is useful for testing thread dumps, heap dumps, and monitoring tools.
 */
public class DeadlockSample {
    private static final Object LOCK_A = new Object();
    private static final Object LOCK_B = new Object();
    public static void main(String[] args) {
        // Thread 1 tries to lock A then lock B
        Thread t1 = new Thread(() -> {
            synchronized (LOCK_A) {
                System.out.println("Thread-1: Acquired LOCK_A");
                sleep(100); // small delay to increase the chance of deadlock
                System.out.println("Thread-1: Waiting for LOCK_B...");
                synchronized (LOCK_B) {
                    System.out.println("Thread-1: Acquired LOCK_B");
                }
            }
        });
        // Thread 2 tries to lock B then lock A
        Thread t2 = new Thread(() -> {
            synchronized (LOCK_B) {
                System.out.println("Thread-2: Acquired LOCK_B");
                sleep(100); // small delay to increase the chance of deadlock
                System.out.println("Thread-2: Waiting for LOCK_A...");
                synchronized (LOCK_A) {
                    System.out.println("Thread-2: Acquired LOCK_A");
                }
            }
        });
        // Start both threads
        t1.start();
        t2.start();
    }
    /**
     * Helper method to sleep without throwing checked exceptions.
     */
    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            // Ignore interruptions for this test program
        }
    }
}
[root@quiz tmp]#
[root@quiz tmp]# javac DeadlockSample.java
[root@quiz tmp]# java DeadlockSample
Thread-1: Acquired LOCK_A
Thread-2: Acquired LOCK_B
Thread-1: Waiting for LOCK_B...
Thread-2: Waiting for LOCK_A...

まとめ

Javaアプリケーションのパフォーマンス問題は、レスポンス遅延やスループット低下、CPUやメモリの高負荷などさまざまな形で現れます。問題の原因を特定するためには、以下のダンプを状況に応じて使い分けることが重要です。

  • スレッドダンプ
    • スレッドごとの状態やロック状況を確認可能。
    • デッドロックや待機中のスレッドの特定に有効。
    • 軽量で取得が容易なため、本番環境でも比較的安全に使用可能。
  • ヒープダンプ
    • JVMが使用しているメモリ全体のスナップショット。
    • メモリリークやオブジェクトの過剰保持を解析できる。
    • 取得時にはアプリケーションが一時停止する場合があり、本番での取得には注意が必要。
  • コアダンプ
    • JVMやネイティブレベルでの異常終了時に生成される。
    • JVM内部のクラッシュ原因や、OSレベルの状態を確認可能。
    • 取得や解析には専門知識が必要で、通常は最後の手段として利用される。

各ダンプはそれぞれ特徴や取得コストが異なるため、問題の性質や環境に応じて適切に選択し、複数のダンプを組み合わせて解析することで、原因特定と再発防止に役立てられます。

コメント