官方的JDK21的JFR介绍:
https://docs.oracle.com/javacomponents/jmc-5-4/jfr-runtime-guide/comline.htm#
https://docs.oracle.com/en/java/javase/21/docs/specs/man/jfr.html。
1.JCMD 启动JFR
1.1 JCMD启动JFR任务
使用jcmd启动JFR的最简单的用法如下:
jcmd <pid> JFR.start name=jfr duration=120s filename=/tmp/jfr.jfr maxsize=250MB
它代表了启动一个name为jfr的JFR任务且JFR任务持续时间为120s,并且到120s过期之后会自动将JFR数据导出到/tmp/jfr.jfr,maxsize=250MB(默认就是250MB)代表JFR文件最多执行250MB时将会自动结束。
如果遇到了以下问题,需要将用户切换到启动目标进程的用户下(必须是进程目标用户才能attach并使用jcmd工具),再次执行dump即可。(例如su tomcat)
com.sun.tools.attach.AttachNotSupportedException: Unable to open socket file /proc/397/root/tmp/.java_pid397: target process 397 doesn't respond within 10500ms or HotSpot VM not loaded
at jdk.attach/sun.tools.attach.VirtualMachineImpl.<init>(VirtualMachineImpl.java:99)
at jdk.attach/sun.tools.attach.AttachProviderImpl.attachVirtualMachine(AttachProviderImpl.java:58)
at jdk.attach/com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:207)
at jdk.jcmd/sun.tools.jcmd.JCmd.executeCommandForPid(JCmd.java:113)
at jdk.jcmd/sun.tools.jcmd.JCmd.main(JCmd.java:97)
1.2 对JFR任务进行dump
在duration时间过期之前,如果想要dump下来name=jfr这个任务的JFR文件,可以使用如下的命令进行随时进行提前dump。
jcmd <pid> JFR.dump name=jfr
如果我指定duration=120s,那么我在第10s执行dump的结果就是[0, 10s]之内的JFR采集的结果,如果我在第20s执行dump的结果就是[0, 20s]之内的JFR的采集的结果。
JFR任务dump的结果会存放到哪里呢?
- 可以通过JFR.dump时,指定filename代表JFR数据dump的文件位置;
- 如果JFR.dump不指定filename的话,会沿用JFR.start时的filename;
- 如果JFR.start时也没指定filename的话,将会根据时间戳自动生成一个文件名进行dump。
1.3 提前结束JFR任务
如果在duration到期之前,可以使用JFR.stop命令提前关闭JFR,指定name=jfr代表关闭name=jfr这样的一个JFR任务。
jcmd <pid> JFR.stop name=jfr
1.4 JFR指定采样的事件
在JAVA_HOME/lib/jfr/目录下,存放了两个jfr的事件配置的配置文件,一个叫default.jmc,一个叫profile.jmc。
通过JFR.start默认使用的是default.jfc,如果需要更详细的事件采样,可以使用profile.jfc,指定配置文件可以基于settings参数进行配置。
jcmd <pid> JFR.start name=jfr duration=120s filename=/tmp/jfr.jfr maxsize=250MB settings=profile
如果需要自定义采样的名称的话,可以自定义settings配置文件
jcmd <pid> JFR.start name=jfr duration=120s filename=/tmp/jfr.jfr maxsize=250MB settings=/path/to/settings.jfc
1.5 基于SpringBootActuator暴露快速JFR端点
@WebEndpoint(id = "jfr")
public class JfrEndpoint {
private static final Logger logger = LoggerFactory.getLogger(JfrEndpoint.class);
/**
* 加锁
*/
private final Lock lock = new ReentrantLock();
private final int timeoutSeconds;
public JfrEndpoint() {
this(10);
}
public JfrEndpoint(int timeoutSeconds) {
this.timeoutSeconds = timeoutSeconds;
}
/**
* 跑JFR任务
*
* @param fileName jfr任务输出的文件
* @param duration 采样时间间隔(默认为120s), 当时间过期时JFR事件数据会自动dump到这个文件
* @param settings settings(自定义jfr配置文件, 可以是default/profile/...)
* @return JFR任务输出的文件
*/
@ReadOperation
public ResponseEntity<Object> profiler(@Nullable String fileName,
@Nullable Long duration,
@Nullable String settings) {
final String fileNameToUse = Optional.ofNullable(fileName).orElse("profiler.jfr");
final Long durationToUse = Optional.ofNullable(duration).orElse(120L);
final String settingsToUse = Optional.ofNullable(settings).orElse("default");
try {
if (lock.tryLock(timeoutSeconds, TimeUnit.MILLISECONDS)) {
try {
// 在Java10可以通过RuntimeMXBean直接获取pid, Java8只能通过切取
final String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
final ProcessBuilder processBuilder = new ProcessBuilder();
final String destinationFilePath = String.format("/tmp/%s", fileNameToUse);
Files.deleteIfExists(Paths.get(destinationFilePath));
processBuilder.command("jcmd", pid, "JFR.start", "name=jfr", String.format("duration=%ss", durationToUse), String.format("filename=%s", destinationFilePath), String.format("settings=%s", settingsToUse));
final Process process = processBuilder.start();
try (InputStream eis = process.getErrorStream(); InputStream is = process.getInputStream()) {
final String message = StreamUtils.copyToString(is, StandardCharsets.UTF_8);
final String errorMessage = StreamUtils.copyToString(eis, StandardCharsets.UTF_8);
logger.info("JfrEndpoint.profiler, {}", message);
if (!process.waitFor(10, TimeUnit.SECONDS)) {
throw new IllegalStateException(String.format("jcmd process start jfr error, %s", errorMessage));
}
}
TimeUnit.SECONDS.sleep(durationToUse);
return ResponseEntity.ok()
// 下载附件的文件名称(使用WebEndpointResponse实现不了这个功能)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileNameToUse + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(new FileSystemResource(destinationFilePath));
} finally {
lock.lock();
}
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("jfr lock error");
} catch (Throwable ex) {
logger.error("jfr profile error", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
2.通过JVM启动参数启动JFR
JVM启动方式配置方式和JCMD基本上完全一致,区别在于JCMD当中,指定多个参数是使用空格分割的自定义参数,命令行启动参数的方式启动是使用的,分隔多个自定义参数。
参考命令如下:
-XX:StartFlightRecording=name=jfr_profiler,maxage=1d,maxsize=1GB,filename=/home/q/www/default.qunar.com/logs/profiler.jfr,duration=600s,dumponexit=true
3.两种启动JFR方式对比
启动方式 | 动态性 | 能否采集启动时数据 | 对线上机器性能影响 | 排查问题便利性 |
---|---|---|---|---|
CMD方式启动 | JVM运行时动态开启 | 不能(JCMD一般在Java应用启动之后才能用) | 小,指定机器开启 | 不一定能抓到问题现场,只能抓一些时机进行分析 |
JVM参数启动 | JVM启动时指定 | 能 | 比较大,对一个环境当中的每台机器均生效,会占用机器一些CPU和内存 | 可以,JFR随时在线上跑着,对于一些特殊的线上问题很方便排查 |