JVM関連ツールを使ってJVMの実行情報を見よう
次の題材はJVMの情報取得や設定変更をするツールです。JDK(Java Development Kit)を配置した(言い換えるとJAVA_HOMEに設定した)ディレクトリ配下のbinディレクトリにはJVMの情報取得や設定変更ができるツールがあります。binディレクトリを見てみましょう。Javaのバージョンは17(Eclipse TemurinのJDK 17.0.2+8)です。28個のコマンドがあります。
$ cd $JAVA_HOME/bin $ ls jar java javadoc jcmd jdb jdeps jhsdb jinfo jmap jpackage jrunscript jstack jstatd rmiregistry jarsigner javac javap jconsole jdeprscan jfr jimage jlink jmod jps jshell jstat keytool serialver
本記事ではこの中からJVMに関連するツールを紹介します。以下の2つです。
- jcmd
- jhsdb
jcmd
jcmdコマンド(以下jcmd)はJVMの情報取得や設定に関して広い範囲をカバーします。jcmdがアクセスできる対象はローカルプロセスのみですが、そうであっても非常に便利なコマンドで使う頻度も多いです。トラブルシュートにもとても役立つコマンドで、ぜひ知っておきましょう。ただしjcmdはカバーする範囲が広い分、コマンドの実行方法が複雑です。
Javaアプリケーションの実行環境であるJVMからさまざまな情報を取得するためには、Javaの以前のバージョンでは例えばjstackやjmapなど取得したい情報に合わせてコマンドを使い分ける必要がありました。jcmdはそういったコマンドの代替であり、現在ではjcmdを使うだけでさまざまな情報を取得できるようになっています。さらにjcmdは情報を取得できるだけでなく、実行に関する設定の追加や変更もできます。
本記事の前半で紹介したUnified Loggingのログ出力設定は、今まで実行時のオプションで指定していました。もし実行した後にログ出力設定の一部を変えたいとするとどうすればよいでしょうか。実行時のオプションを変更してアプリケーションを再実行することが最初に思い浮かぶかと思います。ただし、この方法ではアプリケーションは一度停止します。停止しても問題ないアプリケーションもあるでしょうし、そうでないものもあるでしょう。
ULの設定変更はjcmdから実行することもできます。jcmdは実行中のアプリケーションとやり取りをします。そのためアプリケーションを実行したままULのログ設定を変更できるため、アプリケーションを停止させる必要がありません。早速jcmdでのULログ設定を試してみましょう。以下のプログラムを用意します。
class LongSleep { public static void main(String... args) throws Exception { Thread.sleep(300000); } }
単にスリープするだけのものです。LongSleep.javaというファイル名で保存し、実行します。スリープするプログラムなので実行するとしばらく実行中になります。その間に別のコマンドプロンプトなどから以下のコマンドを実行します。
$ jcmd 87476 jdk.compiler/com.sun.tools.javac.launcher.Main LongSleep.java (個々の状況により上の数字や出力行数が変わります) $ jcmd 87476 VM.log output="sample.log" 87476: Command executed successfully (87476という数字の部分は最初のコマンドの出力にあるLongSleep.javaに対応した数字を指定します)
ここまで実行できたら最初のLongSleepプログラムを停止させてもよいです。このプログラムを実行したディレクトリを見るとsample.log
というファイルができています。このファイルの中を見るとULのログが出力できていることがわかります。今試したことを整理します。ULログの設定をせずにアプリケーションを実行したあと、アプリケーションを実行したままjcmdを使ってULログの出力を新たに設定しました。その結果ファイルにULログが出力できたことを確認しました。
では実行したコマンドの内容を詳しく見ていきましょう。最初にjcmd
とだけ入力して実行しました。この場合実行中のJavaアプリケーションのプロセスとそのプロセスIDを出力します。先ほどの例にあった87476というのはプロセスIDの数値だったのです。なおJDKにはプロセスIDを調べるためのコマンドとして以前からjpsコマンドがありました。jcmdとjpsのどちらを使ってもプロセスIDを調べられます。ただし出力内容や指定できるオプションに違いがあります。
jcmdでJavaプロセスの一覧を出す以外の機能を使う場合、jcmd
と入力した次に対象とするプロセスIDを指定します。jcmd <プロセスIDまたはメインクラス> <コマンド>
という形式です。先ほどの例ではjcmd 87476 VM.log
と入力しました。そのためコマンドはVM.log
です。さらにコマンドは.
の前の部分をドメイン、後ろの部分を操作と言います。VM.log
ではVMがドメイン、logが操作です。ドメインで関連する操作がまとめられています。VMドメインの操作にはJVMの実行情報取得や設定変更があります。VMドメインの操作は以下のものがあります。
- VM.classloader_stats
- VM.class_hierarchy
- VM.command_line
- VM.dynlibs
- VM.info
- VM.log
- VM.flags
- VM.native_memory
- VM.print_touched_methods
- VM.set_flag
- VM.stringtable
- VM.symboltable
- VM.systemdictionary
- VM.system_properties
- VM.uptime
- VM.version
各操作の詳細については省略します。ドメインはVMドメインを含め以下のものがあります(※厳密にはドメインではなく別個の命令としてPerfCounter.printもあります)。
ドメイン | 内容 |
---|---|
VM | JVMの実行情報取得、設定変更 |
Compiler | JITコンパイラ関連 |
GC | ガベージコレクション関連 |
Thread | 現状スレッドダンプ取得のみ |
JVMTI | JVM Tool Interface関連、例えばJVMTIエージェントのロード |
JFR | JDK Flight Recorder関連 |
ManagementAgent | JMXエージェント関連 |
ドメインの内容を見ると利用価値の高いものばかりです。各ドメインにある操作の一覧は公式リファレンスを参照するとよいでしょう。もしくはjcmd <プロセスIDまたはメインクラス> help
を実行するとそのプロセスに対して実行できるコマンドの一覧を出力します。
$ jcmd 59756 help 59756: The following commands are available: Compiler.CodeHeap_Analytics Compiler.codecache (以下省略)
なお各操作で指定できるオプションなど使い方を調べたいときはjcmd <プロセスIDまたはメインクラス> help <コマンド>
とコマンドの前にhelp
を入力して実行するとそのコマンドの使い方を出力します。
$ jcmd 16655 help Thread.print 16655: Thread.print Print all threads with stacktraces. Impact: Medium: Depends on the number of threads. Permission: java.lang.management.ManagementPermission(monitor) Syntax : Thread.print [options] Options: (options must be specified using theor = syntax) -l : [optional] print java.util.concurrent locks (BOOLEAN, false) -e : [optional] print extended thread information (BOOLEAN, false)
jcmdを使う機会はアプリケーション運用時に障害が発生した場合などです。障害が発生したとすれば再度同じ現象が起こったときに上記のThread.print
でスレッドダンプを取ったり、JFRドメインの操作を使ってすぐにフライトレコードを取り始めたりできます。jcmdはJavaエンジニアにとっての使い勝手の良い調査キットと言えます。jcmdが作られたことで同様のことができるコマンドjstackやjinfo、jmapは使わなくなりました。
jhsdb
OpenJDKのJVMであるHotSpot VMはC++で書かれています。そのためJVMがクラッシュした場合C++とJava両方のコードを見なければならないことが多いです。C++側はGDBやLLDBなどのネイティブデバッガを使って調べますが、その情報を元にJava側を調べるときに使うツールがHSDBです。HSDBとはHotSpot Debuggerを意味します。HSDBにはGUI版とCLI版があり、jhsdbコマンドで指定してどちらも起動できます。jhsdbコマンドのヘルプを見ます。
$ jhsdb --help clhsdb command line debugger hsdb ui debugger debugd --help to get more information jstack --help to get more information jmap --help to get more information jinfo --help to get more information jsnap --help to get more information
jhsdb
の形式で実行します。本記事ではhsdb、debugdの2つのmode(以下モード)を解説します。jstackとjmap、jinfoはそれぞれbinディレクトリにある同名のコマンドと同じ機能です。またjsnapはパフォーマンスカウンタ情報を取得するためのものです。
モード | 内容 |
---|---|
hsdb | GUI版のHSDBを起動する |
clhsdb | CLI版のHSDBを起動する |
debugd | デバッグサーバを起動する |
jstack | スタックトレースを出力する |
jmap | ヒープ情報を出力する |
jinfo | 実行時のオプションやシステムプロパティの値を取得、設定する |
jsnap | パフォーマンスカウンタ情報を出力する |
hsdbモードを使ってみましょう。GUI版でもCUI版でもHSDBを活用する方法は次の3つです。
- 実行中のJavaアプリケーションのプロセスIDを指定してHSDBを接続する
- クラッシュの際のコアファイルと使用したjavaコマンドを指定してHSDBに読み込ませる
- 起動しているデバッグサーバを指定してHSDBを接続する
実行時のオプションとして上記の内容を指定するか、HSDB起動後に上記の内容を指定します。本記事では1と3のケースを試します。jcmdの説明で使用したLongSleepのプログラムを実行しましょう。その後jcmdでプロセスIDを調べ、以下のコマンドを実行します。
$ jhsdb hsdb --pid 44228
GUI版のHSDBが起動し、以下のような画面を表示します。なおmacOSではSIP(System Integrity Protection)を無効にしなければなりません。SIPが有効の状態だとHSDBからアプリケーションに接続しようとするとエラーが発生します。SIPを無効にする方法はmacOSのリカバリモードに入り、ターミナルでcsrutil disable
と実行し、再起動します。なお、リカバリモードへの入り方はいわゆるM1 MacとIntel版Macで異なりますので注意してください。
HSDBのよくある使い方としては、HSDBでアドレスを確認したあとGDBやLLDBなどのネイティブデバッガでそのアドレスを使うというものです。この記事でそこまで解説すると記事の趣旨やスコープから大きく外れてしまうので、今回は簡略化してHSDBでクラスのアドレスを調べ、さらにインスペクタでそのアドレスを入力して情報を取得します。HSDBのメニューからTools > Class Browserを選択します。Class Browser画面が表示するリストの一番上に今回作成し実行したLongSleepクラスがあります。そのリンクを押し、画面下部のclass LongSleep @0x0000000800d2c000
といった部分の@
より後ろのアドレス部分に着目します。テキストとしてコピーしておくとよいでしょう。なおこのアドレス部分は実行環境によって異なります。
次にメニューからTools > Inspectorを選択します。Inspector画面のAddress / C++ Expressionのテキストボックスに先ほど着目したアドレスを入力しEnterキーを押します。すると以下のような画面になります。
Inspector画面が表示する内容はLongSleepクラスのJavaではなくC++での表現です。HSDBがJavaとC++の世界を橋渡しするというイメージを持っていただければよいでしょう。JVMがクラッシュしてコアダンプを出力したときなどはGDBやLLDBなどのネイティブデバッガと合わせてHSDBを使うことで障害解析が進むことも多いです。
ここまでHSDBをローカルのJavaプロセスに接続していました。HSDBはローカルだけでなくリモートからも利用できます。そのためには対象となるマシンでHSDBをリモートデバッグサーバとして起動します。下の図のように別のマシンでクライアントとしてHSDBを起動し、リモートデバッグサーバに接続してその情報を閲覧できます。
解析対象となるマシンにGUI環境がないけれどGUI版のHSDBを使いたいなど、そのマシン上で作業できない理由があることもあるでしょう。解析対象のマシンとクライアントとなるマシンの間にOSなどプラットフォームの違いがあっても大丈夫です。試してみましょう。
リモートデバッグサーバとしてWindowsマシンを、HSDBのクライアントとしてmacOS(Apple M1チップ)を使用しました。Windows上でLongSleepプログラムを実行しjcmdでプロセスIDを調べたあと、デバッグサーバを起動します。ホスト名はjyukutyo-windowsと指定しました。ホスト名は皆さんのネットワークの設定状況に合わせて変更してください。以下のようなメッセージが出力されればデバッグサーバを起動できています。
> jhsdb debugd --pid 80465 --hostname jyukutyo-windows Attaching to process ID 80465 and starting RMI services, please wait... Debugger attached and RMI services started.
次にクライアントであるmacOSの方でHSDBを起動しデバッグサーバに接続します。
$ jhsdb hsdb --connect jyukutyo-windows
ローカルで接続したときと同様、以下のような画面を表示します。以降の操作方法はローカルのときと同じです。注意点としてアプリケーション、リモートデバッグサーバ、クライアントで使用するJavaのバージョンを完全に一致させておくことが望ましいです。たとえマイナーバージョンしか違っていなくてもJVMの実装内に違いがあるため、正確に情報が見られないこともあります。異なるバージョンを使用すると警告メッセージが出ます。なおオプション-Dsun.jvm.hotspot.runtime.VM.disableVersionCheck
を使用すればこのチェックを無効にできますが、チェックしないというだけで正確に情報が見られないことに変わりはありません。
またjava.rmi.ConnectExceptionなどの例外が出て接続できないときは、WindowsはC:\Windows\System32\drivers\etc\hosts
、LinuxやmacOSは/etc/hosts
の内容を確認し適切に設定してください。デバッグサーバのホスト名とIPアドレスを両方のマシンに設定します。ただしデバッグサーバをmacOS(Apple M1チップ、Intelチップどちらでも)にするとクライアントから接続する際に以下の例外が発生します。
Exception in thread "AWT-EventQueue-0" java.lang.ClassCastException: class sun.jvm.hotspot.debugger.remote.RemoteDebuggerClient cannot be cast to class sun.jvm.hotspot.debugger.bsd.BsdDebuggerLocal (sun.jvm.hotspot.debugger.remote.RemoteDebuggerClient and sun.jvm.hotspot.debugger.bsd.BsdDebuggerLocal are in module jdk.hotspot.agent of loader 'app') at jdk.hotspot.agent/sun.jvm.hotspot.runtime.bsd_aarch64.BsdAARCH64JavaThreadPDAccess.getThreadProxy(BsdAARCH64JavaThreadPDAccess.java:136) (以下中略)
アプリケーションの多くはmacOS以外で稼働していると思います。このようにHSDBはリモートから接続できることも知っておくと障害発生時に使う機会があるでしょう。
まとめ
本記事ではJVMのログを出力するUnified Logging(UL)とJDKに含まれるツールの中からjcmdとjhsdbを紹介しました。ULはJava 9からですので今後多くのアプリケーションがJava 8から新しいバージョンに移行する際に今回の内容が役に立つでしょう。jcmdとjhsdbは障害発生に備えて身につけておきたいツールです。
さて、この連載はJVMの内部を文章で解説するものではなく、その情報を取得するさまざまなツールの利用を通じてJVMについての知識を深めることを目的としています。連載を継続するためには、ここまで読んでくださった皆さんの(ポジティブな)お声がぜひとも必要です! この画面にあるツイートやシェア、ブックマークボタンからフィードバックをいただけましたらうれしいです。次回はJITコンパイラとその関連ツールを使ってJITコンパイルの情報を取得するという内容です。次回もお楽しみに!