この記事は、Jakarta EE / Java EE Advent Calendar 2025 の 17 日目の記事です。
「Javaアプリのトラブル調査で、より詳細な情報を採取したい。」
「それも、ラクに設定できて、性能影響が抑えられる方法はないだろうか。」
そんなときは、JDK Flight Recorder (JFR) を試してみるとよいかもしれません。
JDK Flight Recorder (JFR) とは
JDK Flight Recorder (JFR) は、Java VM プロセスに関する詳細情報を収集できる、JDKの標準機能です。
低負荷で詳細な情報を採取できる と言われています。
Java SE アプリでJFRを有効にしてみる
以下のように、処理に1 - 1000 ms程度かかる RandomSleep.java があるとします。
import java.util.Random;
public class Main {
public static void main(String... args) {
RandomSleep proc = new RandomSleepImpl();
proc.execute();
}
interface RandomSleep {
void execute();
}
public static class RandomSleepImpl implements RandomSleep {
public void execute() {
// Random number between 1 and 1000
Random random = new Random();
int randomNumber = random.nextInt(1000) + 1;
try {
System.out.println("Sleep for " + randomNumber + " ms.");
Thread.sleep(randomNumber);
} catch (InterruptedException e) {
}
}
}
}
# java RandomSleep.java
Sleep for 87 ms.
このアプリでJFRを有効にするには、-XX:StartFlightRecording:filename=<filepath> オプションを追加して実行するだけで実現できます。
# java -XX:StartFlightRecording:filename=/work/act-oss/demo-jfr-se/ RandomSleep.java
[0.389s][info][jfr,startup] Started recording 1. No limit specified, using maxsize=250MB as default.
[0.389s][info][jfr,startup]
[0.389s][info][jfr,startup] Use jcmd 2936686 JFR.dump name=1 to copy recording data to file.
Sleep for 234 ms.
Java VM プロセスが終了すると、指定のパスにJFRログファイル(.jfr)が出力されます。
# find /work/act-oss/demo-jfr-se/ -name "*.jfr"
/work/act-oss/demo-jfr-se/hotspot-pid-2936686-id-1-2025_12_10_16_16_04.jfr
JFRログファイルの中には、以下のように様々な情報が集まっています。
JFRログファイルは、上記のように JDK Mission Control (JMC) を用いてGUIで可視化したり、jfr print コマンドで情報を出力したりして、参照できます。
Jakarta EE アプリでJFRを有効にしてみる
以下のような Jakarta Servlet のアプリで、JFRを有効にしてみます。
package com.example.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Random;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@WebServlet("/RandomSleepServlet")
public class RandomSleepServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setContentType("text/plain; charset=UTF-8");
PrintWriter responseWriter = response.getWriter();
RandomSleep proc = new RandomSleepImpl();
proc.execute();
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
doGet(request, response);
}
interface RandomSleep {
void execute();
}
public static class RandomSleepImpl implements RandomSleep {
public void execute() {
// Random number between 1 and 1000
Random random = new Random();
int randomNumber = random.nextInt(1000) + 1;
try {
System.out.println("Sleep for " + randomNumber + " ms.");
Thread.sleep(randomNumber);
} catch (InterruptedException e) {
}
}
}
}
このServletを含むWebアプリ(.war)を、Jakarta EEに準拠したランタイムOSSであるGlassFishで動かします。
以下の手順では、2つのJava VM プロセス(GlassFish Server クラスター配下のインスタンス)でアプリを動かします。
各インスタンスでは、HTTPリクエストをポート28080/28081で受け付ける設定となっています。
# asadmin start-domain
...
Successfully started the domain : domain1
domain Location: /opt/glassfish8/glassfish/domains/domain1
Log File: /opt/glassfish8/glassfish/domains/domain1/logs/server.log
Admin Port: 4,848
Command start-domain executed successfully.
# asadmin create-cluster cluster-1
Command create-cluster executed successfully.
# asadmin create-local-instance --cluster cluster-1 cluster-1-1
...
HTTP_LISTENER_PORT=28080
...
Command create-local-instance executed successfully.
# asadmin create-local-instance --cluster cluster-1 cluster-1-2
...
HTTP_LISTENER_PORT=28081
...
Command create-local-instance executed successfully.
# asadmin deploy --target cluster-1 RandomSleepWebApp.war
Application deployed with name RandomSleepWebApp.
Command deploy executed successfully.
このとき、JFRを有効にするには、Java SE アプリ同様の設定を追加して起動するだけで実現できます。
# asadmin create-jvm-options --target cluster-1 -XX\\\:StartFlightRecording=filename\\\=\\\$\\\{com.sun.aas.instanceRoot\\\}/logs
Created 1 option(s)
Command create-jvm-options executed successfully.
# asadmin start-cluster cluster-1
Command start-cluster executed successfully.
# for i in {1..10}; do for port in 28080 28081; do \
curl http://localhost:${port}/RandomSleepWebApp/RandomSleepServlet; \
done; done
Java VM プロセスが終了すると、指定のパスにJFRログファイル(.jfr)が出力されます。
# asadmin stop-cluster cluster-1
Command stop-cluster executed successfully.
# find /opt/glassfish8/glassfish/nodes/localhost-domain1/ -name "*.jfr"
/opt/glassfish8/glassfish/nodes/localhost-domain1/cluster-1-1/logs/hotspot-pid-3430092-id-1-2025_12_11_18_41_11.jfr
/opt/glassfish8/glassfish/nodes/localhost-domain1/cluster-1-2/logs/hotspot-pid-3430080-id-1-2025_12_11_18_41_11.jfr
以下のように、GlassFishのサーバーログ(server.log)と照らし合わせつつ、JFRログファイルから詳細情報を参照できるようになります。
[2025-12-11T18:40:46.747188+09:00] [GF 8.0.0-M14] [INFO] [] [jakarta.enterprise.logging.stdout] [tid: _ThreadID=56 _ThreadName=http-listener-1(1)] [levelValue: 800] [[
Sleep for 407 ms.]]
[2025-12-11T18:40:48.064102+09:00] [GF 8.0.0-M14] [INFO] [] [jakarta.enterprise.logging.stdout] [tid: _ThreadID=72 _ThreadName=http-listener-1(3)] [levelValue: 800] [[
Sleep for 803 ms.]]
[2025-12-11T18:40:49.131526+09:00] [GF 8.0.0-M14] [INFO] [] [jakarta.enterprise.logging.stdout] [tid: _ThreadID=74 _ThreadName=http-listener-1(5)] [levelValue: 800] [[
Sleep for 483 ms.]]
[2025-12-11T18:40:50.466758+09:00] [GF 8.0.0-M14] [INFO] [] [jakarta.enterprise.logging.stdout] [tid: _ThreadID=71 _ThreadName=http-listener-1(2)] [levelValue: 800] [[
Sleep for 571 ms.]]
[2025-12-11T18:40:51.898990+09:00] [GF 8.0.0-M14] [INFO] [] [jakarta.enterprise.logging.stdout] [tid: _ThreadID=73 _ThreadName=http-listener-1(4)] [levelValue: 800] [[
Sleep for 625 ms.]]
[2025-12-11T18:40:53.435862+09:00] [GF 8.0.0-M14] [INFO] [] [jakarta.enterprise.logging.stdout] [tid: _ThreadID=56 _ThreadName=http-listener-1(1)] [levelValue: 800] [[
Sleep for 846 ms.]]
[2025-12-11T18:40:54.336978+09:00] [GF 8.0.0-M14] [INFO] [] [jakarta.enterprise.logging.stdout] [tid: _ThreadID=72 _ThreadName=http-listener-1(3)] [levelValue: 800] [[
Sleep for 808 ms.]]
[2025-12-11T18:40:56.154131+09:00] [GF 8.0.0-M14] [INFO] [] [jakarta.enterprise.logging.stdout] [tid: _ThreadID=74 _ThreadName=http-listener-1(5)] [levelValue: 800] [[
Sleep for 761 ms.]]
[2025-12-11T18:40:57.790818+09:00] [GF 8.0.0-M14] [INFO] [] [jakarta.enterprise.logging.stdout] [tid: _ThreadID=71 _ThreadName=http-listener-1(2)] [levelValue: 800] [[
Sleep for 983 ms.]]
[2025-12-11T18:40:59.406193+09:00] [GF 8.0.0-M14] [INFO] [] [jakarta.enterprise.logging.stdout] [tid: _ThreadID=73 _ThreadName=http-listener-1(4)] [levelValue: 800] [[
Sleep for 914 ms.]]
JFRのチューニング
JFRでは、他にも様々なオプションを指定できます。
例えば、記録期間を限定したり、ファイルサイズを制限したりすることで、実行環境への影響を抑えられないか検討できます。
- 参考: 富士通製品で提供しているJDK 8のドキュメント
- https://software.fujitsu.com/jp/manual/manualfiles/m230004/b1ws1414/03z200/b1414-00-08-03-01.html
- JDKのバージョンやベンダーによってデフォルト設定などが異なる可能性あり
おわりに
JFRは、オプションを1つ追加するだけで利用できるため、非常にお手軽です。
Jakarta EE 実行環境の様々な情報を収集してくれます。
次回は、Jakarta EE環境でより応用の利くJFR活用方法として、自分でカスタマイズした情報をJFRログファイルに出力してみます。
付録
Jakarta EE 実行環境
# cat /etc/redhat-release
Red Hat Enterprise Linux release 8.10 (Ootpa)
# java --version
openjdk 21.0.9 2025-10-21 LTS
OpenJDK Runtime Environment Temurin-21.0.9+10 (build 21.0.9+10-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.9+10 (build 21.0.9+10-LTS, mixed mode, sharing)
# asadmin version
Version = Eclipse GlassFish 8.0.0-M14 (commit: 66cb8e8b4c6ead7ac3e7705e330613b36818f161)
Command version executed successfully.
- インストール物
参考情報
- Eclipse GlassFish Reference Manual
- Flight Recorder API Programmer’s Guide
- Interstage Application Server V13.1.0 チューニングガイド

