JCMD工具导出JVM线程栈(JSTACK)

针对两种需求,提供对应的实现方式。 输出线程栈信息到命令行。 输出线程栈信息到文件。 1.输出线程栈Ø

针对两种需求,提供对应的实现方式。

  • 输出线程栈信息到命令行。
  • 输出线程栈信息到文件。

1.输出线程栈命令行

输出线程栈到命令可以使用如下命令

jcmd <pid> Thread.print

使用如下命令查看帮助文档

jcmd <pid> help Thread.print

# 帮助文档如下
Print all threads with stacktraces.

Impact: Medium: Depends on the number of threads.

Syntax : Thread.print [options]

Options: (options must be specified using the <key> or <key>=<value> syntax)
        -l : [optional] print java.util.concurrent locks (BOOLEAN, false)
        -e : [optional] print extended thread information (BOOLEAN, false)

常见带参数的命令如下(一般不带参数的命令足够使用)

jcmd 397 Thread.print -l -e

# -l参数可以打印关于锁的更详细的信息, 包括该锁的持有线程, 锁的内存地址, 锁的当前状态
# -e参数可以打印线程的扩展信息,1.该线程当前持有的锁和正在等待的锁; 2.不仅包括Java栈帧还输出涉及本地方法调用和其他系统级栈帧。

2.将线程栈导出到文件

jdk21以上版本

将线程栈输出到文件可以使用如下命令

# 将线程栈导出到stackinfo.txt文件当中
jcmd <pid> Thread.dump_to_file stackinfo.txt

使用如下命令查看帮助文档

jcmd <pid> help Thread.dump_to_file

# 帮助文档如下
Thread.dump_to_file
Dump threads, with stack traces, to a file in plain text or JSON format.

Impact: Medium: Depends on the number of threads.

Syntax : Thread.dump_to_file [options] <filepath>

Arguments:
        filepath :  The file path to the output file (STRING, no default value)

Options: (options must be specified using the <key> or <key>=<value> syntax)
        -overwrite : [optional] May overwrite existing file (BOOLEAN, false)
        -format : [optional] Output format ("plain" or "json") (STRING, plain)

可以使用如下这样的一些命令

# 将线程栈导出到stackinfo.txt文件当中
jcmd <pid> Thread.dump_to_file stackinfo.txt
# 和上面命令等价, 默认就是"-format=plain"
jcmd <pid> Thread.dump_to_file -format=plain stackinfo.txt
# 使用json格式输出线程栈并导出到stackinfo.txt文件当中
jcmd <pid> Thread.dump_to_file -format=json stackinfo.txt

jdk21以下版本(jdk8/jdk17/…)

jdk21以下,没有Thread.dump_to_file命令,只能使用Thread.print命令。

可以通过重定向的方式写入到文件

jcmd <pid> Thread.print > jstack.txt

3.基于SpringBootActuator暴露JStack端点

@WebEndpoint(id = "jstack")
public class JStackEndpoint {

    private static final Logger logger = LoggerFactory.getLogger(JStackEndpoint.class);

    /**
     * 加锁
     */
    private final Lock lock = new ReentrantLock();

    private final int timeoutSeconds;

    public JStackEndpoint() {
        this(10);
    }

    public JStackEndpoint(int timeoutSeconds) {
        this.timeoutSeconds = timeoutSeconds;
    }

    @ReadOperation
    public ResponseEntity<Object> doJStack(@Nullable String type, @Nullable String fileName) {
        final String typeToUse = Optional.ofNullable(type).orElse("file");
        final String fileNameToUse = Optional.ofNullable(fileName).orElse("stackinfo.txt");
        final String resourcePath = String.format("/tmp/%s", fileNameToUse);

        try {
            if (lock.tryLock(timeoutSeconds, TimeUnit.SECONDS)) {
                try {
                    // 在Java10可以通过RuntimeMXBean直接获取pid, Java8只能通过切取
                    final String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
                    final ProcessBuilder processBuilder = new ProcessBuilder();

                    processBuilder.command("jcmd", pid, "Thread.print", "-l", "-e");

                    final Process process = processBuilder.start();
                    try (InputStream eis = process.getErrorStream(); InputStream is = process.getInputStream()) {

                        final JStackResult result;
                        if ("text".equals(typeToUse)) {
                            result = new JStackResult(StreamUtils.copyToString(is, StandardCharsets.UTF_8), MediaType.TEXT_MARKDOWN);
                        } else if ("file".equals(typeToUse)) {
                            Files.deleteIfExists(Paths.get(resourcePath));
                            try (OutputStream os = new FileOutputStream(resourcePath, false)) {
                                StreamUtils.copy(is, os);
                                result = new JStackResult(new FileSystemResource(resourcePath), MediaType.APPLICATION_OCTET_STREAM);
                            }
                        } else {
                            result = new JStackResult(String.format("Unsupported type(%s) for jstack endpoint", typeToUse), MediaType.TEXT_PLAIN);
                        }
                        // 必须先读缓冲区, 再waitFor等待进程退出
                        if (process.waitFor() != 0) {
                            throw new RuntimeException(String.format("Wait jcmd process error, %s", StreamUtils.copyToString(eis, StandardCharsets.UTF_8)));
                        }
                        return ResponseEntity.ok()
                                // 下载附件的文件名称(使用WebEndpointResponse实现不了这个功能)
                                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileNameToUse + "\"")
                                .contentType(result.mediaType)
                                .body(result.result);
                    }
                } finally {
                    lock.unlock();
                }
            }
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("jstack lock error");
        } catch (Throwable ex) {
            logger.error("do jstack error", ex);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    static class JStackResult {

        private final Object result;

        private final MediaType mediaType;

        public JStackResult(Object result, MediaType mediaType) {
            this.result = result;
            this.mediaType = mediaType;
        }
    }
}

Comment