CLOVER🍀

That was when it all began.

jcmd(jstack)を使わずに、スレッドダンプを取得する

これは、なにをしたくて書いたもの?

jcmdやjstackなどを使わずに、Javaアプリケーションのスレッドダンプを取得する方法を、自分でも試しておこうかなと。

ThreadMXBean#dumpAllThreads​とThread#getAllStackTraces

やり方としては、ThreadMXBeanやThreadクラスを利用すればよさそうです。

ThreadMXBeanを使った例としては、Spring Boot Actuatorのスレッドダンプ用のエンドポイントがこちらを使っています。

https://github.com/spring-projects/spring-boot/blob/v2.3.0.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/ThreadDumpEndpoint.java

ThreadMXBeanのJavadocは、こちら。

ThreadMXBean (Java SE 11 & JDK 11 )

また、GraalVMのSubstrate VMではJMXが使えないので、Threadクラスから全スレッドのStackTraceElementを取得するという
方法もあります。

こちらを使っているのは、Quarkusですね。SignalHandlerを使って、スレッドダンプを取れるようにしてあります。

https://github.com/quarkusio/quarkus/blob/1.5.0.Final/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java

https://github.com/quarkusio/quarkus/blob/1.5.0.Final/core/runtime/src/main/java/io/quarkus/runtime/graal/DiagnosticPrinter.java

ThreadのJavadocは、こちら。

Thread (Java SE 11 & JDK 11 )

このあたりを使っていってみましょう。

環境

今回の環境は、こちら。

$ java --version
openjdk 11.0.7 2020-04-14
OpenJDK Runtime Environment (build 11.0.7+10-post-Ubuntu-2ubuntu218.04)
OpenJDK 64-Bit Server VM (build 11.0.7+10-post-Ubuntu-2ubuntu218.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.7, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-101-generic", arch: "amd64", family: "unix"

準備

お題としては、JAX-RSリソースクラスに、スレッドダンプをJSONで返すようなプログラムを書いてみたいと思います。

JAX-RSの実装としては、RESTEasyを使用しましょう。

pom.xmlの依存関係は、こちら。

    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-vertx</artifactId>
            <version>4.5.3.Final</version>
        </dependency>

        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-core</artifactId>
            <version>3.9.1</version>
        </dependency>

        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jackson2-provider</artifactId>
            <version>4.5.3.Final</version>
        </dependency>
    </dependencies>

サンプルコード

作成したソースコードを載せていきます。

まずは、スレッドダンプを取得するJAX-RSリソースクラス。
src/main/java/org/littlewings/resteasy/ThreadDumpResource.java

package org.littlewings.resteasy;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("thread")
public class ThreadDumpResource {
    @GET
    @Path("mx-bean")
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, Object> threadMXBean() {
        ThreadInfo[] threadInfoArray = ManagementFactory.getThreadMXBean().dumpAllThreads(true, true);

        return Map.of("thread_dump", threadInfoArray);
    }

    @GET
    @Path("all-threads-stacktraces")
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, Object> allThreadsStackTraces() {
        Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();

        return allStackTraces
                .entrySet()
                .stream()
                .collect(Collectors.toMap(e -> e.getKey().getName(), e -> {
                    Thread thread = e.getKey();
                    return Map.of(
                            "thread_info",
                            Map.of("id", thread.getId(), "priority", thread.getPriority(), "state", thread.getState(), "group", thread.getThreadGroup(), "alive", thread.isAlive(), "daemon", thread.isDaemon()),
                            "stacktraces",
                            e.getValue()
                    );
                }));
    }
}

こちらが、ThreadMXBeanを使っている方ですね。ThreadMXBean#dumpAllThreadsメソッドを使用します。

    @GET
    @Path("mx-bean")
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, Object> threadMXBean() {
        ThreadInfo[] threadInfoArray = ManagementFactory.getThreadMXBean().dumpAllThreads(true, true);

        return Map.of("thread_dump", threadInfoArray);
    }

2つの引数の意味は、ロックモニターを出力するかどうかと、ロックされたすべてのシンクロナイザーを出力するかどうかです。

https://docs.oracle.com/javase/jp/11/docs/api/java.management/java/lang/management/ThreadMXBean.html#dumpAllThreads(boolean,boolean)

ThreadMXBean#dumpAllThreadsメソッドの戻り値は、ThreadInfoの配列です。ここから、スレッドの情報やスタックトレースなどを
取得することができます。

ThreadInfo (Java SE 11 & JDK 11 )

もうひとつは、Thread#getAllStackTracesを使う方です。

    @GET
    @Path("all-threads-stacktraces")
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, Object> allThreadsStackTraces() {
        Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();

        return allStackTraces
                .entrySet()
                .stream()
                .collect(Collectors.toMap(e -> e.getKey().getName(), e -> {
                    Thread thread = e.getKey();
                    return Map.of(
                            "thread_info",
                            Map.of("id", thread.getId(), "priority", thread.getPriority(), "state", thread.getState(), "group", thread.getThreadGroup(), "alive", thread.isAlive(), "daemon", thread.isDaemon()),
                            "stacktraces",
                            e.getValue()
                    );
                }));
    }

こちらは、全スレッドに対して、Threadをキー、StackTraceElementの配列を値にしたMapを返します。

https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/lang/Thread.html#getAllStackTraces()

今回のお題ではこのMapをそのままJacksonでJSONに変換してもらってもいいのですが、それだとThread.Stateなどが
出力されないので、ちょっとだけ整形しておきました。

        return allStackTraces
                .entrySet()
                .stream()
                .collect(Collectors.toMap(e -> e.getKey().getName(), e -> {
                    Thread thread = e.getKey();
                    return Map.of(
                            "thread_info",
                            Map.of("id", thread.getId(), "priority", thread.getPriority(), "state", thread.getState(), "group", thread.getThreadGroup(), "alive", thread.isAlive(), "daemon", thread.isDaemon()),
                            "stacktraces",
                            e.getValue()
                    );
                }));

あとは、JAX-RSサーバーを起動するクラスを用意するだけです。
src/main/java/org/littlewings/resteasy/Server.java

package org.littlewings.resteasy;

import java.util.List;

import org.jboss.logging.Logger;
import org.jboss.resteasy.plugins.server.vertx.VertxJaxrsServer;
import org.jboss.resteasy.plugins.server.vertx.VertxResteasyDeployment;
import org.jboss.resteasy.spi.ResteasyDeployment;

public class Server {
    public static void main(String... args) {
        Logger logger = Logger.getLogger(Server.class);

        ResteasyDeployment deployment = new VertxResteasyDeployment();
        deployment.setResources(List.of(new ThreadDumpResource()));

        VertxJaxrsServer server = new VertxJaxrsServer();
        server.setDeployment(deployment);
        server.setRootResourcePath("");
        server.setPort(8080);

        server.start();

        logger.infof("Server startup");

        System.console().readLine("> Press Enter stop.");

        server.stop();
    }
}

確認する

それでは、確認してみましょう。

まずは、作成したサーバーを起動。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.resteasy.Server

ThreadMXBeanの方から確認してみます。

$ curl -s localhost:8080/thread/mx-bean | jq

結果は、こんな感じ。

{
  "thread_dump": [
    {
      "threadName": "main",
      "threadId": 1,
      "blockedTime": -1,
      "blockedCount": 0,
      "waitedTime": -1,
      "waitedCount": 1,
      "lockName": "java.lang.Thread@411755fc",
      "lockOwnerId": -1,
      "lockOwnerName": null,
      "daemon": false,
      "inNative": false,
      "suspended": false,
      "threadState": "WAITING",
      "priority": 5,
      "stackTrace": [
        {
          "classLoaderName": null,
          "moduleName": "java.base",
          "moduleVersion": "11.0.7",
          "methodName": "wait",
          "fileName": "Object.java",
          "lineNumber": -2,
          "className": "java.lang.Object",
          "nativeMethod": true
        },
        {
          "classLoaderName": null,
          "moduleName": "java.base",
          "moduleVersion": "11.0.7",
          "methodName": "join",
          "fileName": "Thread.java",
          "lineNumber": 1305,
          "className": "java.lang.Thread",
          "nativeMethod": false
        },

〜省略〜

    {
      "threadName": "vert.x-eventloop-thread-7",
      "threadId": 24,
      "blockedTime": -1,
      "blockedCount": 19,
      "waitedTime": -1,
      "waitedCount": 0,
      "lockName": null,
      "lockOwnerId": -1,
      "lockOwnerName": null,
      "daemon": false,
      "inNative": false,
      "suspended": false,
      "threadState": "RUNNABLE",
      "priority": 5,
      "stackTrace": [
        {          "moduleVersion": "11.0.7",
          "methodName": "dumpThreads0",
          "fileName": "ThreadImpl.java",
          "lineNumber": -2,
          "className": "sun.management.ThreadImpl",
          "nativeMethod": true
        },
        {
          "classLoaderName": null,
          "moduleName": "java.management",
          "moduleVersion": "11.0.7",
          "methodName": "dumpAllThreads",
          "fileName": "ThreadImpl.java",
          "lineNumber": 502,
          "className": "sun.management.ThreadImpl",
          "nativeMethod": false
        },
        {
          "classLoaderName": null,
          "moduleName": "java.management",
          "moduleVersion": "11.0.7",
          "methodName": "dumpAllThreads",
          "fileName": "ThreadImpl.java",
          "lineNumber": 490,
          "className": "sun.management.ThreadImpl",
          "nativeMethod": false
        },
        {
          "classLoaderName": null,
          "moduleName": null,
          "moduleVersion": null,
          "methodName": "threadMXBean",
          "fileName": "ThreadDumpResource.java",
          "lineNumber": 20,
          "className": "org.littlewings.resteasy.ThreadDumpResource",
          "nativeMethod": false
        },
        {
          "classLoaderName": null,
          "moduleName": "java.base",
          "moduleVersion": "11.0.7",
          "methodName": "invoke0",
          "fileName": "NativeMethodAccessorImpl.java",
          "lineNumber": -2,
          "className": "jdk.internal.reflect.NativeMethodAccessorImpl",
          "nativeMethod": true
        },
        {
          "classLoaderName": null,
          "moduleName": "java.base",
          "moduleVersion": "11.0.7",
          "methodName": "invoke",
          "fileName": "NativeMethodAccessorImpl.java",
          "lineNumber": 62,
          "className": "jdk.internal.reflect.NativeMethodAccessorImpl",
          "nativeMethod": false
        },
        {
          "classLoaderName": null,
          "moduleName": "java.base",
          "moduleVersion": "11.0.7",
          "methodName": "invoke",
          "fileName": "DelegatingMethodAccessorImpl.java",
          "lineNumber": 43,
          "className": "jdk.internal.reflect.DelegatingMethodAccessorImpl",
          "nativeMethod": false
        },
        {
          "classLoaderName": null,
          "moduleName": "java.base",
          "moduleVersion": "11.0.7",
          "methodName": "invoke",
          "fileName": "Method.java",
          "lineNumber": 566,
          "className": "java.lang.reflect.Method",
          "nativeMethod": false
        },

〜省略〜

次は、Threadクラスを使った方。

$ curl -s localhost:8080/thread/all-threads-stacktraces | jq

結果は、こんな感じ。

{
  "thread_dump": [
    {
      "threadName": "main",
      "threadId": 1,
      "blockedTime": -1,
      "blockedCount": 0,
      "waitedTime": -1,
      "waitedCount": 1,
      "lockName": "java.lang.Thread@411755fc",
      "lockOwnerId": -1,
      "lockOwnerName": null,
      "daemon": false,
      "inNative": false,
      "suspended": false,
      "threadState": "WAITING",
      "priority": 5,
      "stackTrace": [
        {
          "classLoaderName": null,
          "moduleName": "java.base",
          "moduleVersion": "11.0.7",
          "methodName": "wait",
          "fileName": "Object.java",
          "lineNumber": -2,
          "className": "java.lang.Object",
          "nativeMethod": true
        },
        {
          "classLoaderName": null,
          "moduleName": "java.base",
          "moduleVersion": "11.0.7",
          "methodName": "join",
          "fileName": "Thread.java",
          "lineNumber": 1305,
          "className": "java.lang.Thread",
          "nativeMethod": false
        },

〜省略〜

    {
      "threadName": "vert.x-eventloop-thread-13",
      "threadId": 30,
      "blockedTime": -1,
      "blockedCount": 15,
      "waitedTime": -1,
      "waitedCount": 0,
      "lockName": null,
      "lockOwnerId": -1,
      "lockOwnerName": null,
      "daemon": false,
      "inNative": false,
      "suspended": false,
      "threadState": "RUNNABLE",
      "priority": 5,
      "stackTrace": [
        {
          "classLoaderName": null,
          "moduleName": "java.management",
          "moduleVersion": "11.0.7",
          "methodName": "dumpThreads0",
          "fileName": "ThreadImpl.java",
          "lineNumber": -2,
          "className": "sun.management.ThreadImpl",
          "nativeMethod": true
        },
        {
          "classLoaderName": null,
          "moduleName": "java.management",
          "moduleVersion": "11.0.7",
          "methodName": "dumpAllThreads",
          "fileName": "ThreadImpl.java",
          "lineNumber": 502,
          "className": "sun.management.ThreadImpl",
          "nativeMethod": false
        },
        {
          "classLoaderName": null,
          "moduleName": "java.management",
          "moduleVersion": "11.0.7",
          "methodName": "dumpAllThreads",
          "fileName": "ThreadImpl.java",
          "lineNumber": 490,
          "className": "sun.management.ThreadImpl",
          "nativeMethod": false
        },
        {
          "classLoaderName": null,
          "moduleName": null,
          "moduleVersion": null,
          "methodName": "threadMXBean",
          "fileName": "ThreadDumpResource.java",
          "lineNumber": 20,
          "className": "org.littlewings.resteasy.ThreadDumpResource",
          "nativeMethod": false
        },
        {
          "classLoaderName": null,
          "moduleName": "java.base",
          "moduleVersion": "11.0.7",
          "methodName": "invoke0",
          "fileName": "NativeMethodAccessorImpl.java",
          "lineNumber": -2,
          "className": "jdk.internal.reflect.NativeMethodAccessorImpl",
          "nativeMethod": true
        },
        {
          "classLoaderName": null,
          "moduleName": "java.base",
          "moduleVersion": "11.0.7",
          "methodName": "invoke",
          "fileName": "NativeMethodAccessorImpl.java",
          "lineNumber": 62,
          "className": "jdk.internal.reflect.NativeMethodAccessorImpl",
          "nativeMethod": false
        },

〜省略〜

動作確認はできました、と。まあ、JSONだとスレッドダンプを見るにしてはちょっと辛いですが。

Spring Boot ActuatorもQuarkusも、頑張って自分でフォーマットしているので、マネするならこんな感じかなぁと。

https://github.com/spring-projects/spring-boot/blob/v2.3.0.RELEASE/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/management/PlainTextThreadDumpFormatter.java

https://github.com/quarkusio/quarkus/blob/1.5.0.Final/core/runtime/src/main/java/io/quarkus/runtime/graal/DiagnosticPrinter.java#L28-L73