これは、なにをしたくて書いたもの?
jcmdやjstackなどを使わずに、Javaアプリケーションのスレッドダンプを取得する方法を、自分でも試しておこうかなと。
ThreadMXBean#dumpAllThreadsとThread#getAllStackTraces
やり方としては、ThreadMXBeanやThreadクラスを利用すればよさそうです。
ThreadMXBeanを使った例としては、Spring Boot Actuatorのスレッドダンプ用のエンドポイントがこちらを使っています。
ThreadMXBeanのJavadocは、こちら。
ThreadMXBean (Java SE 11 & JDK 11 )
また、GraalVMのSubstrate VMではJMXが使えないので、Threadクラスから全スレッドのStackTraceElementを取得するという
方法もあります。
こちらを使っているのは、Quarkusですね。SignalHandlerを使って、スレッドダンプを取れるようにしてあります。
ThreadのJavadocは、こちら。
このあたりを使っていってみましょう。
環境
今回の環境は、こちら。
$ 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つの引数の意味は、ロックモニターを出力するかどうかと、ロックされたすべてのシンクロナイザーを出力するかどうかです。
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も、頑張って自分でフォーマットしているので、マネするならこんな感じかなぁと。